From 412e02ef0083e4ae37a8a5958dee9368951a4ae2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 12 Nov 2025 04:46:02 +0400 Subject: [PATCH] Gift auctions --- .../Telegram-iOS/en.lproj/Localizable.strings | 88 ++ .../Sources/AccountContext.swift | 4 +- submodules/TelegramApi/Sources/Api0.swift | 1 + submodules/TelegramApi/Sources/Api29.swift | 88 +- submodules/TelegramApi/Sources/Api30.swift | 50 ++ submodules/TelegramBaseController/BUILD | 6 + .../Sources/GiftAuctionAccessoryPanel.swift | 297 +++++++ .../Sources/TelegramBaseController.swift | 72 ++ .../ApiUtils/TelegramMediaAction.swift | 3 +- .../ApiUtils/TelegramMediaWebpage.swift | 5 + .../SyncCore_TelegramMediaAction.swift | 17 +- .../SyncCore_TelegramMediaWebpage.swift | 64 ++ .../Payments/StarGiftsAuctions.swift | 42 +- .../Resources/PresentationResourceKey.swift | 3 + .../Resources/PresentationResourcesChat.swift | 14 +- .../Sources/ServiceMessageStrings.swift | 6 +- .../Sources/ButtonComponent.swift | 32 +- .../CameraScreen/Sources/CameraScreen.swift | 1 + .../Sources/CaptureControlsComponent.swift | 8 + .../CameraScreen/Sources/ModeComponent.swift | 3 +- .../ChatMessageAttachedContentNode.swift | 9 +- .../ChatMessageGiftBubbleContentNode.swift | 4 +- .../ChatMessageInteractiveMediaNode.swift | 37 + .../ChatMessageWebpageBubbleContentNode.swift | 23 + .../ChatRecentActionsControllerNode.swift | 2 + .../Sources/GiftCompositionComponent.swift | 16 +- .../Components/Gifts/GiftItemComponent/BUILD | 1 + .../Sources/GiftItemComponent.swift | 156 +++- .../Sources/GiftOptionsScreen.swift | 69 +- .../Sources/ChatGiftPreviewItem.swift | 2 +- .../Sources/GiftAuctionAcquiredScreen.swift | 77 +- .../Sources/GiftAuctionActiveBidsScreen.swift | 774 ++++++++++++++++++ .../Sources/GiftAuctionBidScreen.swift | 651 ++++++++++----- .../Sources/GiftAuctionInfoScreen.swift | 78 +- .../Sources/GiftAuctionViewScreen.swift | 750 +++++++++++++---- .../Sources/GiftViewScreen.swift | 2 +- .../Sources/ShareWithPeersScreen.swift | 3 +- .../Sources/WallpaperPreviewMedia.swift | 46 ++ .../Auction/BidLarge.imageset/Contents.json | 12 + .../Auction/BidLarge.imageset/bid_120.pdf | Bin 0 -> 4220 bytes .../Auction/BidMedium.imageset/Contents.json | 12 + .../Auction/BidMedium.imageset/bid_30.pdf | Bin 0 -> 4212 bytes .../Auction/BidSmall.imageset/Contents.json | 12 + .../Auction/BidSmall.imageset/bid_20.pdf | Bin 0 -> 4227 bytes .../TelegramUI/Sources/OpenResolvedUrl.swift | 18 + submodules/TelegramUI/Sources/OpenUrl.swift | 16 + .../Sources/SharedAccountContext.swift | 4 + .../UrlHandling/Sources/UrlHandling.swift | 12 + 48 files changed, 3061 insertions(+), 529 deletions(-) create mode 100644 submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift create mode 100644 submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Auction/BidLarge.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Auction/BidLarge.imageset/bid_120.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Auction/BidMedium.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Auction/BidMedium.imageset/bid_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Auction/BidSmall.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Auction/BidSmall.imageset/bid_20.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 7d5dae46d3..f3c6b86322 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15176,3 +15176,91 @@ Error: %8$@"; "LoginEmail.Success.Title" = "Email added"; "LoginEmail.Success.Text" = "Your account is now protected!"; + +"Notification.GiftAuction.Acquired" = "You've successfully bought a gift in the auction for %@."; + +"Gift.Auction.Auction" = "auction"; +"Gift.Auction.Description" = "Top %@ bidders will get a **%@** this round. [Learn More >]()"; +"Gift.Auction.Ended" = "Auction ended"; +"Gift.Auction.FirstSale" = "First Sale"; +"Gift.Auction.LastSale" = "Last Sale"; +"Gift.Auction.Started" = "Started"; +"Gift.Auction.Ends" = "Ends"; +"Gift.Auction.AveragePrice" = "Average Price"; +"Gift.Auction.CurrentRound" = "Current Round"; +"Gift.Auction.Round" = "%@ of %@"; +"Gift.Auction.Availability" = "Availability"; +"Gift.Auction.AvailabilityOf" = "%@ of %@ left"; +"Gift.Auction.Join" = "Join Auction"; +"Gift.Auction.TimeLeftHours" = "{h}h {m}m left"; +"Gift.Auction.TimeLeftMinutes" = "{m}:{s} left"; +"Gift.Auction.ItemsBought_1" = "item bought"; +"Gift.Auction.ItemsBought_any" = "items bought"; +"Gift.Auction.ForSaleOnTelegram" = "for sale on Telegram"; +"Gift.Auction.ForSaleOnFragment" = "for sale on Fragment"; +"Gift.Auction.AveragePriceInfo" = "**%1$@** is the average sale price for %2$@ gifts."; +"Gift.Auction.Stars_1" = "%@ Star"; +"Gift.Auction.Stars_any" = "%@ Stars"; + +"Gift.AuctionBid.Title" = "Place a Bid"; +"Gift.AuctionBid.MinimumBid" = "minimum bid"; +"Gift.AuctionBid.UntilNext" = "until next round"; +"Gift.AuctionBid.Left" = "left"; +"Gift.AuctionBid.YourBid" = "your bid"; +"Gift.AuctionBid.Winning" = "You're winning"; +"Gift.AuctionBid.Outbid" = "You've been outbid"; +"Gift.AuctionBid.BidPreview" = "Your bid will be"; +"Gift.AuctionBid.TopWinners" = "Top 3 Winners"; +"Gift.AuctionBid.PlaceBid" = "Place a %@ Bid"; +"Gift.AuctionBid.AddToBid" = "Add %@ to Your Bid"; + +"Gift.Auction.Context.About" = "About"; +"Gift.Auction.Context.CopyLink" = "Copy Link"; +"Gift.Auction.Context.Share" = "Share"; + +"Gift.ActiveAuctions.Title_1" = "%@ Active Auction"; +"Gift.ActiveAuctions.Title_any" = "%@ Active Auctions"; +"Gift.ActiveAuctions.Round" = "Round %@ of %@"; +"Gift.ActiveAuctions.Winning" = "Your bid **%@** is ranked **#%@**"; +"Gift.ActiveAuctions.Outbid" = "Your bid **%@** is outbid"; +"Gift.ActiveAuctions.RaiseBid" = "Raise Bid"; + +"Gift.Acquired.Title_1" = "%@ Item Bought"; +"Gift.Acquired.Title_any" = "%@ Items Bought"; +"Gift.Acquired.Round" = "Round #%@"; +"Gift.Acquired.Recipient" = "Recipient"; +"Gift.Acquired.Date" = "Date"; +"Gift.Acquired.AcceptedBid" = "Accepted Bid"; +"Gift.Acquired.Top" = "TOP %@"; + +"Gift.Options.Gift.Auction" = "auction"; + +"Story.Camera.Live" = "Live"; + +"Chat.Auction" = "Gift Auction"; +"Chat.Auction.ViewResults" = "VIEW RESULTS"; +"Chat.Auction.Join" = "JOIN"; +"Chat.Auction.Finished" = "finished"; +"Chat.Auction.Gifts_1" = "%@ gift"; +"Chat.Auction.Gifts_any" = "%@ gifts"; + +"Story.Privacy.StartLiveAs" = "Start Live As"; + +"ChatList.Auctions.ActiveAuction_1" = "Active Auction"; +"ChatList.Auctions.ActiveAuction_any" = "%@ Active Auctions"; +"ChatList.Auctions.Status.Single.Winning" = "You're winning (%@ place)."; +"ChatList.Auctions.Status.Single.PlaceFirst" = "%@st"; +"ChatList.Auctions.Status.Single.PlaceSecond" = "%@nd"; +"ChatList.Auctions.Status.Single.PlaceThird" = "%@rd"; +"ChatList.Auctions.Status.Single.PlaceNTh" = "%@th"; +"ChatList.Auctions.Status.Single.Outbid" = "You've been outbid."; +"ChatList.Auctions.Status.Many.WinningAll" = "You're winning all of them."; +"ChatList.Auctions.Status.Many.Outbid_1" = "You've been outbid in %@ of them."; +"ChatList.Auctions.Status.Many.Outbid_any" = "You've been outbid in %@ of them."; +"ChatList.Auctions.Status.Many.OutbidAll" = "You've been outbid in all of them."; +"ChatList.Auctions.View" = "View"; + +"Gift.Auction.Ongoing.Title" = "One Auction at a Time"; +"Gift.Auction.Ongoing.Text" = "You’ve already bid in this gift auction for **%@**."; +"Gift.Auction.Ongoing.TextYourself" = "You’ve already bid in this gift auction for yourself."; +"Gift.Auction.Ongoing.View" = "View"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index ca4058b102..d6a58698a8 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -328,6 +328,7 @@ public enum ResolvedUrl { case premiumGiftCode(slug: String) case premiumMultiGift(reference: String?) case collectible(gift: StarGift.UniqueGift?) + case auction(auction: GiftAuctionContext?) case messageLink(link: TelegramResolvedMessageLink?) case stars case ton @@ -1424,7 +1425,8 @@ public protocol SharedAccountContext: AnyObject { func makeGiftAuctionInfoScreen(context: AccountContext, gift: StarGift, completion: (() -> Void)?) -> ViewController func makeGiftAuctionBidScreen(context: AccountContext, auctionContext: GiftAuctionContext) -> ViewController func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext) -> ViewController - + func makeGiftAuctionActiveBidsScreen(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)?) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 13f8e76d6b..89a3f87057 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -1247,6 +1247,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[555358088] = { return Api.WebPage.parse_webPageEmpty($0) } dict[1930545681] = { return Api.WebPage.parse_webPageNotModified($0) } dict[-1328464313] = { return Api.WebPage.parse_webPagePending($0) } + dict[55150251] = { return Api.WebPageAttribute.parse_webPageAttributeStarGiftAuction($0) } dict[835375875] = { return Api.WebPageAttribute.parse_webPageAttributeStarGiftCollection($0) } dict[1355547603] = { return Api.WebPageAttribute.parse_webPageAttributeStickerSet($0) } dict[781501415] = { return Api.WebPageAttribute.parse_webPageAttributeStory($0) } diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index b46d5caec3..f48b75e2b2 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,5 +1,6 @@ public extension Api { indirect enum WebPageAttribute: TypeConstructorDescription { + case webPageAttributeStarGiftAuction(gift: Api.StarGift, endDate: Int32, centerColor: Int32, edgeColor: Int32, textColor: Int32) case webPageAttributeStarGiftCollection(icons: [Api.Document]) case webPageAttributeStickerSet(flags: Int32, stickers: [Api.Document]) case webPageAttributeStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?) @@ -8,6 +9,16 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .webPageAttributeStarGiftAuction(let gift, let endDate, let centerColor, let edgeColor, let textColor): + if boxed { + buffer.appendInt32(55150251) + } + gift.serialize(buffer, true) + serializeInt32(endDate, buffer: buffer, boxed: false) + serializeInt32(centerColor, buffer: buffer, boxed: false) + serializeInt32(edgeColor, buffer: buffer, boxed: false) + serializeInt32(textColor, buffer: buffer, boxed: false) + break case .webPageAttributeStarGiftCollection(let icons): if boxed { buffer.appendInt32(835375875) @@ -61,6 +72,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .webPageAttributeStarGiftAuction(let gift, let endDate, let centerColor, let edgeColor, let textColor): + return ("webPageAttributeStarGiftAuction", [("gift", gift as Any), ("endDate", endDate as Any), ("centerColor", centerColor as Any), ("edgeColor", edgeColor as Any), ("textColor", textColor as Any)]) case .webPageAttributeStarGiftCollection(let icons): return ("webPageAttributeStarGiftCollection", [("icons", icons as Any)]) case .webPageAttributeStickerSet(let flags, let stickers): @@ -74,6 +87,31 @@ public extension Api { } } + public static func parse_webPageAttributeStarGiftAuction(_ reader: BufferReader) -> WebPageAttribute? { + var _1: Api.StarGift? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StarGift + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.WebPageAttribute.webPageAttributeStarGiftAuction(gift: _1!, endDate: _2!, centerColor: _3!, edgeColor: _4!, textColor: _5!) + } + else { + return nil + } + } public static func parse_webPageAttributeStarGiftCollection(_ reader: BufferReader) -> WebPageAttribute? { var _1: [Api.Document]? if let _ = reader.readInt32() { @@ -1346,53 +1384,3 @@ public extension Api.account { } } -public extension Api.account { - enum SavedRingtone: TypeConstructorDescription { - case savedRingtone - case savedRingtoneConverted(document: Api.Document) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .savedRingtone: - if boxed { - buffer.appendInt32(-1222230163) - } - - break - case .savedRingtoneConverted(let document): - if boxed { - buffer.appendInt32(523271863) - } - document.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .savedRingtone: - return ("savedRingtone", []) - case .savedRingtoneConverted(let document): - return ("savedRingtoneConverted", [("document", document as Any)]) - } - } - - public static func parse_savedRingtone(_ reader: BufferReader) -> SavedRingtone? { - return Api.account.SavedRingtone.savedRingtone - } - public static func parse_savedRingtoneConverted(_ reader: BufferReader) -> SavedRingtone? { - var _1: Api.Document? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Document - } - let _c1 = _1 != nil - if _c1 { - return Api.account.SavedRingtone.savedRingtoneConverted(document: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index d2be22c3b9..de90e1dd0d 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,3 +1,53 @@ +public extension Api.account { + enum SavedRingtone: TypeConstructorDescription { + case savedRingtone + case savedRingtoneConverted(document: Api.Document) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedRingtone: + if boxed { + buffer.appendInt32(-1222230163) + } + + break + case .savedRingtoneConverted(let document): + if boxed { + buffer.appendInt32(523271863) + } + document.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedRingtone: + return ("savedRingtone", []) + case .savedRingtoneConverted(let document): + return ("savedRingtoneConverted", [("document", document as Any)]) + } + } + + public static func parse_savedRingtone(_ reader: BufferReader) -> SavedRingtone? { + return Api.account.SavedRingtone.savedRingtone + } + public static func parse_savedRingtoneConverted(_ reader: BufferReader) -> SavedRingtone? { + var _1: Api.Document? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Document + } + let _c1 = _1 != nil + if _c1 { + return Api.account.SavedRingtone.savedRingtoneConverted(document: _1!) + } + else { + return nil + } + } + + } +} public extension Api.account { enum SavedRingtones: TypeConstructorDescription { case savedRingtones(hash: Int64, ringtones: [Api.Document]) diff --git a/submodules/TelegramBaseController/BUILD b/submodules/TelegramBaseController/BUILD index 52f34d888a..57b114255f 100644 --- a/submodules/TelegramBaseController/BUILD +++ b/submodules/TelegramBaseController/BUILD @@ -27,6 +27,12 @@ swift_library( "//submodules/TelegramNotices:TelegramNotices", "//submodules/TooltipUI:TooltipUI", "//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift b/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift new file mode 100644 index 0000000000..8a05eff35f --- /dev/null +++ b/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift @@ -0,0 +1,297 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import ComponentFlow +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import GiftItemComponent +import AnimatedTextComponent + +private let titleFont = Font.semibold(15.0) +private let subtitleFont = Font.regular(14.0) + +final class GiftAuctionAccessoryPanel: ASDisplayNode { + private let context: AccountContext + private var theme: PresentationTheme + private var strings: PresentationStrings + + private let tapAction: () -> Void + + private let contentNode: ASDisplayNode + + private let title = ComponentView() + private let subtitle = ComponentView() + private let button = ComponentView() + + private let separatorNode: ASDisplayNode + + private var validLayout: (CGSize, CGFloat, CGFloat, Bool)? + private var states: [GiftAuctionContext.State] = [] + private var giftAuctionTimer: SwiftSignalKit.Timer? + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, tapAction: @escaping () -> Void) { + self.context = context + self.theme = theme + self.strings = strings + + self.tapAction = tapAction + + self.contentNode = ASDisplayNode() + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.contentNode) + + self.contentNode.addSubnode(self.separatorNode) + + self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + if let self, let (size, leftInset, rightInset, isHidden) = self.validLayout { + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isHidden: isHidden, transition: .immediate) + } + }, queue: Queue.mainQueue()) + self.giftAuctionTimer?.start() + } + + deinit { + self.giftAuctionTimer?.invalidate() + } + + override func didLoad() { + super.didLoad() + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.view.addGestureRecognizer(tapRecognizer) + } + + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, isHidden: Bool, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, leftInset, rightInset, isHidden) + + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: isHidden ? -size.height : 0.0), size: size)) + transition.updateAlpha(node: self.contentNode, alpha: isHidden ? 0.0 : 1.0) + + + guard self.states.count > 0 else { + return + } + + var titleItems: [AnyComponentWithIdentity] = [] + for auctionState in self.states { + if case let .generic(gift) = auctionState.gift { + titleItems.append(AnyComponentWithIdentity(id: "icon-\(gift.id)", component: AnyComponent( + GiftItemComponent( + context: self.context, + theme: self.theme, + strings: self.strings, + peer: nil, + subject: .starGift(gift: gift, price: ""), + mode: .tableIcon + ) + ))) + } + } + + let titleText: String = self.strings.ChatList_Auctions_ActiveAuction(Int32(self.states.count)) + titleItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor)))))) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent( + HStack(titleItems, spacing: 3.0, alignment: .left) + ), + environment: {}, + containerSize: size + ) + let titleFrame = CGRect(origin: CGPoint(x: 16.0, y: 9.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.contentNode.view.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let subtitleText: String + var subtitleTextColor = self.theme.rootController.navigationBar.secondaryTextColor + var isOutbid = false + + var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] + + if self.states.count == 1, let auctionState = self.states.first { + let place = auctionState.place ?? 1 + 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 + } else { + let placeText: String + let lastDigit = place % 10 + switch lastDigit { + case 1: + placeText = self.strings.ChatList_Auctions_Status_Single_PlaceFirst("\(place)").string + case 2: + placeText = self.strings.ChatList_Auctions_Status_Single_PlaceSecond("\(place)").string + case 3: + placeText = self.strings.ChatList_Auctions_Status_Single_PlaceThird("\(place)").string + default: + placeText = self.strings.ChatList_Auctions_Status_Single_PlaceNTh("\(place)").string + } + subtitleText = self.strings.ChatList_Auctions_Status_Single_Winning(placeText).string + } + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var endTime = currentTime + if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, _, _) = auctionState.auctionState { + endTime = nextRoundDate + } + + let endTimeout = max(0, endTime - currentTime) + + let hours = Int(endTimeout / 3600) + let minutes = Int((endTimeout % 3600) / 60) + let seconds = Int(endTimeout % 60) + + if hours > 0 { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 1))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon1", content: .text(":"))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":"))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } else { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":"))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } + } else { + var outbidCount = 0 + for auctionState in self.states { + let place = auctionState.place ?? 1 + if case let .generic(gift) = auctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound { + outbidCount += 1 + } + } + if outbidCount > 0 { + if outbidCount == self.states.count { + subtitleText = self.strings.ChatList_Auctions_Status_Many_OutbidAll + } else { + subtitleText = self.strings.ChatList_Auctions_Status_Many_Outbid(Int32(outbidCount)) + } + subtitleTextColor = self.theme.list.itemDestructiveColor + isOutbid = true + } else { + subtitleText = self.strings.ChatList_Auctions_Status_Many_WinningAll + } + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "view", content: .text(self.strings.ChatList_Auctions_View))) + } + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: subtitleText, font: subtitleFont, textColor: subtitleTextColor)))), + environment: {}, + containerSize: size + ) + let subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: 29.0), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.contentNode.view.addSubview(subtitleView) + } + subtitleView.frame = subtitleFrame + } + + let buttonSize = self.button.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + color: self.theme.list.itemCheckColors.fillColor, + foreground: self.theme.list.itemCheckColors.foregroundColor, + pressedColor: self.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 14.0, + isShimmering: isOutbid + ), + content: AnyComponentWithIdentity( + id: "content", + component: AnyComponent(HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Premium/Auction/BidSmall", tintColor: self.theme.list.itemCheckColors.foregroundColor))), + AnyComponentWithIdentity(id: "timer", component: AnyComponent( + AnimatedTextComponent( + font: Font.with(size: 15.0, weight: .semibold, traits: .monospacedNumbers), + color: self.theme.list.itemCheckColors.foregroundColor, + items: buttonAnimatedTitleItems, + noDelay: true + ) + )) + ], spacing: 3.0)) + ), + fitToContentWidth: true, + action: { [weak self] in + guard let self else { + return + } + self.tapAction() + } + ) + ), + environment: {}, + containerSize: CGSize(width: size.width, height: 28.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - buttonSize.width - 16.0, y: 14.0), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.contentNode.view.addSubview(buttonView) + } + buttonView.frame = buttonFrame + } + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) + } + + func update(states: [GiftAuctionContext.State]) { + self.states = states + if let (size, leftInset, rightInset, isHidden) = self.validLayout { + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isHidden: isHidden, transition: .immediate) + } + } + + func animateIn(_ transition: ContainedViewLayoutTransition) { + let contentPosition = self.contentNode.layer.position + + transition.animatePosition(node: self.contentNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 56.0)) + + guard let (size, _, _, _) = self.validLayout else { + return + } + + transition.animatePositionAdditive(node: self.separatorNode, offset: CGPoint(x: 0.0, y: size.height)) + } + + func animateOut(_ transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + let contentPosition = self.contentNode.layer.position + transition.animatePosition(node: self.contentNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 56.0), removeOnCompletion: false, completion: { _ in + completion() + }) + + guard let (size, _, _, _) = self.validLayout else { + return + } + + transition.updatePosition(node: self.separatorNode, position: self.separatorNode.position.offsetBy(dx: 0.0, dy: size.height)) + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapAction() + } + } +} diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 53089efce6..5c7dbc2da9 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -89,6 +89,10 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { private var locationBroadcastMessages: [EngineMessage.Id: EngineMessage]? private var locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel? + private var giftAuctionAccessoryPanel: GiftAuctionAccessoryPanel? + private var giftAuctionStates: [GiftAuctionContext.State] = [] + private var giftAuctionDisposable: Disposable? + private var groupCallPanelData: GroupCallPanelData? public private(set) var groupCallAccessoryPanel: GroupCallNavigationAccessoryPanel? @@ -358,6 +362,17 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } } + if let giftAuctionsManager = context.giftAuctionsManager, case .summary = locationBroadcastPanelSource { + self.giftAuctionDisposable = (giftAuctionsManager.state + |> deliverOnMainQueue).start(next: { [weak self] states in + guard let self else { + return + } + self.giftAuctionStates = states + + }) + } + self.presentationDataDisposable = (self.updatedPresentationData.1 |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { @@ -522,6 +537,63 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } } + if !self.giftAuctionStates.isEmpty { + let panelHeight: CGFloat = 56.0 + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) + additionalHeight += panelHeight + panelStartY += panelHeight + + let giftAuctionAccessoryPanel: GiftAuctionAccessoryPanel + if let current = self.giftAuctionAccessoryPanel { + giftAuctionAccessoryPanel = current + transition.updateFrame(node: giftAuctionAccessoryPanel, frame: panelFrame) + giftAuctionAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: transition) + } else { + giftAuctionAccessoryPanel = GiftAuctionAccessoryPanel(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, tapAction: { [weak self] in + guard let self else { + return + } + if !"".isEmpty, self.giftAuctionStates.count == 1, let gift = self.giftAuctionStates.first?.gift, case let .generic(gift) = gift { + if let giftAuctionsManager = self.context.giftAuctionsManager { + let _ = (giftAuctionsManager.auctionContext(for: .giftId(gift.id)) + |> deliverOnMainQueue).start(next: { [weak self] auction in + guard let self, let auction else { + return + } + let controller = self.context.sharedContext.makeGiftAuctionBidScreen(context: self.context, auctionContext: auction) + self.push(controller) + }) + } + } else { + let controller = self.context.sharedContext.makeGiftAuctionActiveBidsScreen(context: self.context) + self.push(controller) + } + }) + if let accessoryPanelContainer = self.accessoryPanelContainer { + accessoryPanelContainer.addSubnode(giftAuctionAccessoryPanel) + } else { + self.navigationBar?.additionalContentNode.addSubnode(giftAuctionAccessoryPanel) + } + self.giftAuctionAccessoryPanel = giftAuctionAccessoryPanel + giftAuctionAccessoryPanel.frame = panelFrame + + giftAuctionAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: .immediate) + if transition.isAnimated { + giftAuctionAccessoryPanel.animateIn(transition) + } + } + giftAuctionAccessoryPanel.update(states: self.giftAuctionStates) + } else if let giftAuctionAccessoryPanel = self.giftAuctionAccessoryPanel { + self.giftAuctionAccessoryPanel = nil + if transition.isAnimated { + giftAuctionAccessoryPanel.animateOut(transition, completion: { [weak giftAuctionAccessoryPanel] in + giftAuctionAccessoryPanel?.removeFromSupernode() + }) + } else { + giftAuctionAccessoryPanel.removeFromSupernode() + } + } + if let locationBroadcastPeers = self.locationBroadcastPeers, let locationBroadcastMode = self.locationBroadcastMode { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 5713821d54..8d2c483acf 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -182,7 +182,6 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe case let .messageActionPrizeStars(flags, stars, transactionId, boostPeer, giveawayMsgId): return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId))) case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars, fromId, peer, savedId, prepaidUpgradeHash, giftMessageId, toId): - let _ = toId let text: String? let entities: [MessageTextEntity]? switch message { @@ -196,7 +195,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe guard let gift = StarGift(apiStarGift: apiGift) else { return nil } - return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId, upgradeSeparate: (flags & (1 << 16)) != 0)) + return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId, upgradeSeparate: (flags & (1 << 16)) != 0, isAuctionAcquired: (flags & (1 << 17)) != 0, toPeerId: toId?.peerId)) case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars): guard let gift = StarGift(apiStarGift: apiGift) else { return nil diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift index b439a8aff7..b08d3f4d91 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift @@ -32,6 +32,11 @@ func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPa var files: [TelegramMediaFile] = [] files = icons.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } return .giftCollection(TelegramMediaWebpageGiftCollectionAttribute(files: files)) + case let .webPageAttributeStarGiftAuction(apiGift, endDate, centerColor, edgeColor, textColor): + guard let gift = StarGift(apiStarGift: apiGift) else { + return nil + } + return .giftAuction(TelegramMediaWebpageGiftAuctionAttribute(gift: gift, endDate: endDate, centerColor: centerColor, edgeColor: edgeColor, textColor: textColor)) case .webPageAttributeStory: return nil } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index e1b89389a1..549e5dd77a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -255,7 +255,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String) case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) - case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool) + case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool, isAuctionAcquired: Bool, toPeerId: EnginePeer.Id?) case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool) case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool) @@ -394,7 +394,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } self = .prizeStars(amount: decoder.decodeInt64ForKey("amount", orElse: 0), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: boostPeerId, transactionId: decoder.decodeOptionalStringForKey("transactionId"), giveawayMessageId: giveawayMessageId) case 44: - self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), prepaidUpgradeHash: decoder.decodeOptionalStringForKey("prepaidUpgradeHash"), giftMessageId: decoder.decodeOptionalInt32ForKey("giftMessageId"), upgradeSeparate: decoder.decodeOptionalBoolForKey("upgradeSeparate") ?? false) + self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), prepaidUpgradeHash: decoder.decodeOptionalStringForKey("prepaidUpgradeHash"), giftMessageId: decoder.decodeOptionalInt32ForKey("giftMessageId"), upgradeSeparate: decoder.decodeOptionalBoolForKey("upgradeSeparate") ?? false, isAuctionAcquired: decoder.decodeOptionalBoolForKey("isAuctionAcquired") ?? false, toPeerId: decoder.decodeOptionalInt64ForKey("toPeerId").flatMap { EnginePeer.Id($0) }) case 45: var resaleAmount: CurrencyAmount? if let amount = decoder.decodeCodable(CurrencyAmount.self, forKey: "resaleAmount") { @@ -728,7 +728,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "giveawayMsgId") } - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, isAuctionAcquired, toPeerId): encoder.encodeInt32(44, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") if let convertStars { @@ -786,6 +786,12 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeNil(forKey: "giftMessageId") } encoder.encodeBool(upgradeSeparate, forKey: "upgradeSeparate") + encoder.encodeBool(isAuctionAcquired, forKey: "isAuctionAcquired") + if let toPeerId { + encoder.encodeInt64(toPeerId.toInt64(), forKey: "toPeerId") + } else { + encoder.encodeNil(forKey: "toPeerId") + } case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned): encoder.encodeInt32(45, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") @@ -920,7 +926,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { return [peerId] case let .prizeStars(_, _, boostPeerId, _, _): return boostPeerId.flatMap { [$0] } ?? [] - case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _): + case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, toPeerId): var peerIds: [PeerId] = [] if let peerId { peerIds.append(peerId) @@ -931,6 +937,9 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { if let releasedBy = gift.releasedBy { peerIds.append(releasedBy) } + if let toPeerId { + peerIds.append(toPeerId) + } return peerIds case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _): var peerIds: [PeerId] = [] diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift index 3f03b7e088..7910601f9d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift @@ -8,6 +8,7 @@ private enum TelegramMediaWebpageAttributeTypes: Int32 { case stickerPack case starGift case giftCollection + case giftAuction } public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { @@ -16,6 +17,7 @@ public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { case stickerPack(TelegramMediaWebpageStickerPackAttribute) case starGift(TelegramMediaWebpageStarGiftAttribute) case giftCollection(TelegramMediaWebpageGiftCollectionAttribute) + case giftAuction(TelegramMediaWebpageGiftAuctionAttribute) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("r", orElse: 0) { @@ -27,6 +29,8 @@ public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { self = .starGift(decoder.decodeObjectForKey("a", decoder: { TelegramMediaWebpageStarGiftAttribute(decoder: $0) }) as! TelegramMediaWebpageStarGiftAttribute) case TelegramMediaWebpageAttributeTypes.giftCollection.rawValue: self = .giftCollection(decoder.decodeObjectForKey("a", decoder: { TelegramMediaWebpageGiftCollectionAttribute(decoder: $0) }) as! TelegramMediaWebpageGiftCollectionAttribute) + case TelegramMediaWebpageAttributeTypes.giftAuction.rawValue: + self = .giftAuction(decoder.decodeObjectForKey("a", decoder: { TelegramMediaWebpageGiftAuctionAttribute(decoder: $0) }) as! TelegramMediaWebpageGiftAuctionAttribute) default: self = .unsupported } @@ -48,6 +52,9 @@ public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { case let .giftCollection(attribute): encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.giftCollection.rawValue, forKey: "r") encoder.encodeObject(attribute, forKey: "a") + case let .giftAuction(attribute): + encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.giftAuction.rawValue, forKey: "r") + encoder.encodeObject(attribute, forKey: "a") } } } @@ -195,6 +202,63 @@ public final class TelegramMediaWebpageGiftCollectionAttribute: PostboxCoding, E } } +public final class TelegramMediaWebpageGiftAuctionAttribute: PostboxCoding, Equatable { + public static func == (lhs: TelegramMediaWebpageGiftAuctionAttribute, rhs: TelegramMediaWebpageGiftAuctionAttribute) -> Bool { + if lhs.gift != rhs.gift { + return false + } + if lhs.endDate != rhs.endDate { + return false + } + if lhs.centerColor != rhs.centerColor { + return false + } + if lhs.edgeColor != rhs.edgeColor { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + return true + } + + public let gift: StarGift + public let endDate: Int32 + public let centerColor: Int32 + public let edgeColor: Int32 + public let textColor: Int32 + + public init( + gift: StarGift, + endDate: Int32, + centerColor: Int32, + edgeColor: Int32, + textColor: Int32 + ) { + self.gift = gift + self.endDate = endDate + self.centerColor = centerColor + self.edgeColor = edgeColor + self.textColor = textColor + } + + public init(decoder: PostboxDecoder) { + self.gift = decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift + self.endDate = decoder.decodeInt32ForKey("endDate", orElse: 0) + self.centerColor = decoder.decodeInt32ForKey("centerColor", orElse: 0) + self.edgeColor = decoder.decodeInt32ForKey("edgeColor", orElse: 0) + self.textColor = decoder.decodeInt32ForKey("textColor", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.gift, forKey: "gift") + encoder.encodeInt32(self.endDate, forKey: "endDate") + encoder.encodeInt32(self.centerColor, forKey: "centerColor") + encoder.encodeInt32(self.edgeColor, forKey: "edgeColor") + encoder.encodeInt32(self.textColor, forKey: "textColor") + } +} + public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let url: String public let displayUrl: String diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift index 919c0f2cf3..e61519538c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift @@ -55,7 +55,7 @@ public final class GiftAuctionContext { } public enum AuctionState: Equatable { - case ongoing(version: Int32, minBidAmount: Int64, bidLevels: [BidLevel], topBidders: [EnginePeer.Id], nextRoundDate: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32) + case ongoing(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [BidLevel], topBidders: [EnginePeer.Id], nextRoundDate: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32) case finished(startDate: Int32, endDate: Int32, averagePrice: Int64) } @@ -90,6 +90,14 @@ public final class GiftAuctionContext { return self.stateValue.get() } + public var currentBidPeerId: EnginePeer.Id? { + if self.myState?.bidAmount != nil { + return self.myState?.bidPeerId + } else { + return nil + } + } + public convenience init(account: Account, gift: StarGift) { self.init(account: account, gift: gift, initialAuctionState: nil, initialMyState: nil, initialTimeout: nil) } @@ -112,7 +120,7 @@ public final class GiftAuctionContext { private var currentVersion: Int32 { var currentVersion: Int32 = 0 - if case let .ongoing(version, _, _, _, _, _, _, _) = self.auctionState { + if case let .ongoing(version, _, _, _, _, _, _, _, _, _) = self.auctionState { currentVersion = version } return currentVersion @@ -130,7 +138,7 @@ public final class GiftAuctionContext { return } - if case let .ongoing(version, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion { + if case let .ongoing(version, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion { } else if let auctionState { self.auctionState = auctionState } @@ -190,9 +198,7 @@ extension GiftAuctionContext.State.AuctionState { init?(apiAuctionState: Api.StarGiftAuctionState) { switch apiAuctionState { case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBidders, nextRoundAt, giftsLeft, currentRound, totalRounds): - let _ = startDate - let _ = endDate - self = .ongoing(version: version, minBidAmount: minBidAmount, bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)), topBidders: topBidders.map { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, nextRoundDate: nextRoundAt, giftsLeft: giftsLeft, currentRound: currentRound, totalRounds: totalRounds) + self = .ongoing(version: version, startDate: startDate, endDate: endDate, minBidAmount: minBidAmount, bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)), topBidders: topBidders.map { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, nextRoundDate: nextRoundAt, giftsLeft: giftsLeft, currentRound: currentRound, totalRounds: totalRounds) case let .starGiftAuctionStateFinished(startDate, endDate, averagePrice): self = .finished(startDate: startDate, endDate: endDate, averagePrice: averagePrice) case .starGiftAuctionStateNotModified: @@ -418,10 +424,10 @@ public class GiftAuctionsManager { private func updateState() { var signals: [Signal] = [] - for (_, auction) in self.auctionContexts { + for auction in self.auctionContexts.values.sorted(by: { $0.gift.giftId < $1.gift.giftId }) { signals.append(auction.state |> mapToSignal { state in - if let state { + if let state, state.myState.bidAmount != nil { return .single(state) } else { return .complete() @@ -434,7 +440,7 @@ public class GiftAuctionsManager { public extension GiftAuctionContext.State { var place: Int32? { - guard case let .ongoing(_, _, bidLevels, _, _, _, _, _) = self.auctionState, let myBid = self.myState.bidAmount, let myBidDate = self.myState.bidDate else { + guard case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _) = self.auctionState, let myBid = self.myState.bidAmount, let myBidDate = self.myState.bidDate else { return nil } var place: Int32 = 1 @@ -445,4 +451,22 @@ public extension GiftAuctionContext.State { } return place } + + var startDate: Int32 { + switch self.auctionState { + case let .ongoing(_, startDate, _, _, _, _, _, _, _, _): + return startDate + case let .finished(startDate, _, _): + return startDate + } + } + + var endDate: Int32 { + switch self.auctionState { + case let .ongoing(_, _, endDate, _, _, _, _, _, _, _): + return endDate + case let .finished(_, endDate, _): + return endDate + } + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 1aa3532958..504d20f5dc 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -268,6 +268,9 @@ public enum PresentationResourceKey: Int32 { case chatMessageAttachedContentHighlightedButtonIconLinkOutgoingWithWallpaper case chatMessageAttachedContentHighlightedButtonIconLinkOutgoingWithoutWallpaper + case chatMessageAttachedContentButtonIconBidIncoming + case chatMessageAttachedContentButtonIconBidOutgoing + case chatCommandPanelArrowImage case sharedMediaFileDownloadStartIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 383e821d94..d2e5ce37d7 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -869,7 +869,7 @@ public struct PresentationResourcesChat { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: theme.chat.message.incoming.accentControlColor) }) } - + public static func chatMessageAttachedContentHighlightedButtonIconLinkIncoming(_ theme: PresentationTheme, wallpaper: Bool) -> UIImage? { let key: PresentationResourceKey = !wallpaper ? PresentationResourceKey.chatMessageAttachedContentHighlightedButtonIconLinkIncomingWithoutWallpaper : PresentationResourceKey.chatMessageAttachedContentHighlightedButtonIconLinkIncomingWithWallpaper return theme.image(key.rawValue, { theme in @@ -877,6 +877,12 @@ public struct PresentationResourcesChat { }) } + public static func chatMessageAttachedContentButtonIconBidIncoming(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIconBidIncoming.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Premium/Auction/BidSmall"), color: theme.chat.message.incoming.accentControlColor) + }) + } + public static func chatMessageAttachedContentButtonIconLinkOutgoing(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIconLinkOutgoing.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: theme.chat.message.outgoing.accentControlColor) @@ -890,6 +896,12 @@ public struct PresentationResourcesChat { }) } + public static func chatMessageAttachedContentButtonIconBidOutgoing(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIconBidOutgoing.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Premium/Auction/BidSmall"), color: theme.chat.message.outgoing.accentControlColor) + }) + } + public static func chatBubbleReplyThumbnailPlayImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleReplyThumbnailPlayImage.rawValue, { theme in return generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index f936684cf8..3855648936 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1171,7 +1171,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = mutableString case .prizeStars: attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor) - case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, upgradeSeparate): + case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, upgradeSeparate, isAuctionAcquired, _): if !forAdditionalServiceMessage { if let text { let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) @@ -1196,7 +1196,9 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, authorName = strings.Notification_StarsGift_UnknownUser peerIds = [] } - if message.id.peerId.isTelegramNotifications && senderId == nil { + if isAuctionAcquired { + attributedString = addAttributesToStringWithRanges(strings.Notification_GiftAuction_Acquired(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else if message.id.peerId.isTelegramNotifications && senderId == nil { attributedString = NSAttributedString(string: strings.Notification_StarsGift_SentSomeone, font: titleFont, textColor: primaryTextColor) } else if message.id.peerId == accountPeerId { attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Self_Bought(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 805f29714e..e20592a3e4 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -381,6 +381,7 @@ public final class ButtonComponent: Component { public let background: Background public let content: AnyComponentWithIdentity + public let fitToContentWidth: Bool public let isEnabled: Bool public let tintWhenDisabled: Bool public let allowActionWhenDisabled: Bool @@ -390,6 +391,7 @@ public final class ButtonComponent: Component { public init( background: Background, content: AnyComponentWithIdentity, + fitToContentWidth: Bool = false, isEnabled: Bool = true, tintWhenDisabled: Bool = true, allowActionWhenDisabled: Bool = false, @@ -398,6 +400,7 @@ public final class ButtonComponent: Component { ) { self.background = background self.content = content + self.fitToContentWidth = fitToContentWidth self.isEnabled = isEnabled self.tintWhenDisabled = tintWhenDisabled self.allowActionWhenDisabled = allowActionWhenDisabled @@ -412,6 +415,9 @@ public final class ButtonComponent: Component { if lhs.content != rhs.content { return false } + if lhs.fitToContentWidth != rhs.fitToContentWidth { + return false + } if lhs.isEnabled != rhs.isEnabled { return false } @@ -538,6 +544,12 @@ public final class ButtonComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - cornerRadius, height: availableSize.height) ) + + var size = availableSize + if component.fitToContentWidth { + size.width = floor(contentSize.width + cornerRadius * 1.5) + } + if let contentView = contentItem.view.view { var animateIn = false var contentTransition = transition @@ -549,7 +561,7 @@ public final class ButtonComponent: Component { contentItem.view.parentState = state } - let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - contentSize.height) * 0.5)), size: contentSize) + let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) * 0.5), y: floorToScreenPixels((size.height - contentSize.height) * 0.5)), size: contentSize) contentTransition.setFrame(view: contentView, frame: contentFrame) contentTransition.setAlpha(view: contentView, alpha: contentAlpha) @@ -557,7 +569,7 @@ public final class ButtonComponent: Component { if animateIn && previousContentItem != nil && !transition.animation.isImmediate { contentView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring) contentView.layer.animateAlpha(from: 0.0, to: contentAlpha, duration: 0.1) - contentView.layer.animatePosition(from: CGPoint(x: 0.0, y: -availableSize.height * 0.15), to: CGPoint(), duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + contentView.layer.animatePosition(from: CGPoint(x: 0.0, y: -size.height * 0.15), to: CGPoint(), duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } @@ -567,7 +579,7 @@ public final class ButtonComponent: Component { previousContentView.layer.animateAlpha(from: contentAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousContentView] _ in previousContentView?.removeFromSuperview() }) - previousContentView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: availableSize.height * 0.35), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + previousContentView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: size.height * 0.35), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) } else { previousContentView.removeFromSuperview() } @@ -587,7 +599,7 @@ public final class ButtonComponent: Component { } let indicatorSize = CGSize(width: 22.0, height: 22.0) transition.setAlpha(view: activityIndicator.view, alpha: 1.0) - activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - indicatorSize.width) / 2.0), y: floorToScreenPixels((size.height - indicatorSize.height) / 2.0)), size: indicatorSize)) } else { if let activityIndicator = self.activityIndicator { self.activityIndicator = nil @@ -608,8 +620,8 @@ public final class ButtonComponent: Component { self.shimmeringView = shimmeringView self.containerView.insertSubview(shimmeringView, at: 0) } - shimmeringView.update(size: availableSize, background: component.background, cornerRadius: component.background.cornerRadius, transition: shimmeringTransition) - shimmeringTransition.setFrame(view: shimmeringView, frame: CGRect(origin: .zero, size: availableSize)) + shimmeringView.update(size: size, background: component.background, cornerRadius: component.background.cornerRadius, transition: shimmeringTransition) + shimmeringTransition.setFrame(view: shimmeringView, frame: CGRect(origin: .zero, size: size)) } else if let shimmeringView = self.shimmeringView { self.shimmeringView = nil shimmeringView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in @@ -636,7 +648,7 @@ public final class ButtonComponent: Component { chromeView.alpha = 0.8 chromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0), isDark: component.background.color.lightness < 0.36, fillColor: .clear) } - chromeTransition.setFrame(view: chromeView, frame: CGRect(origin: .zero, size: availableSize)) + chromeTransition.setFrame(view: chromeView, frame: CGRect(origin: .zero, size: size)) } else if let chromeView = self.chromeView { self.chromeView = nil chromeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in @@ -644,10 +656,10 @@ public final class ButtonComponent: Component { }) } - transition.setPosition(view: self.containerView, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) - transition.setBoundsSize(view: self.containerView, size: availableSize) + transition.setPosition(view: self.containerView, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + transition.setBoundsSize(view: self.containerView, size: size) - return availableSize + return size } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 4a8eccd8f1..149c587202 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1451,6 +1451,7 @@ private final class CameraScreenComponent: CombinedComponent { let captureControls = captureControls.update( component: CaptureControlsComponent( context: component.context, + strings: environment.strings, isTablet: isTablet, isSticker: isSticker, hasGallery: !isSticker && !isAvatar, diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index 64172f4a92..1ea33c9d0d 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -4,6 +4,7 @@ import Display import ComponentFlow import SwiftSignalKit import TelegramCore +import TelegramPresentationData import Photos import LocalMediaResources import CameraButtonComponent @@ -37,6 +38,7 @@ private extension SimpleShapeLayer { } private final class ShutterButtonContentComponent: Component { + let strings: PresentationStrings let isTablet: Bool let hasAppeared: Bool let tintColor: UIColor @@ -49,6 +51,7 @@ private final class ShutterButtonContentComponent: Component { let updateOffsetY: ActionSlot<(CGFloat, ComponentTransition)> init( + strings: PresentationStrings, isTablet: Bool, hasAppeared: Bool, tintColor: UIColor, @@ -60,6 +63,7 @@ private final class ShutterButtonContentComponent: Component { updateOffsetX: ActionSlot<(CGFloat, ComponentTransition)>, updateOffsetY: ActionSlot<(CGFloat, ComponentTransition)> ) { + self.strings = strings self.isTablet = isTablet self.hasAppeared = hasAppeared self.tintColor = tintColor @@ -673,6 +677,7 @@ final class CaptureControlsComponent: Component { } let context: AccountContext + let strings: PresentationStrings let isTablet: Bool let isSticker: Bool let hasGallery: Bool @@ -701,6 +706,7 @@ final class CaptureControlsComponent: Component { init( context: AccountContext, + strings: PresentationStrings, isTablet: Bool, isSticker: Bool, hasGallery: Bool, @@ -728,6 +734,7 @@ final class CaptureControlsComponent: Component { openResolvedPeer: @escaping (EnginePeer) -> Void ) { self.context = context + self.strings = strings self.isTablet = isTablet self.isSticker = isSticker self.hasGallery = hasGallery @@ -1425,6 +1432,7 @@ final class CaptureControlsComponent: Component { Button( content: AnyComponent( ShutterButtonContentComponent( + strings: component.strings, isTablet: component.isTablet, hasAppeared: component.hasAppeared, tintColor: component.tintColor, diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index 70ede1050c..beaed2f1b5 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -14,8 +14,7 @@ extension CameraMode { case .video: return strings.Story_Camera_Video case .live: - //TODO:localize - return "LIVE" + return strings.Story_Camera_Live } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index fee0565330..45ef4deb9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -37,6 +37,7 @@ import EmojiTextAttachmentView public enum ChatMessageAttachedContentActionIcon { case instant case link + case bid } public struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { @@ -323,7 +324,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { var mediaAndFlags = mediaAndFlags if let mediaAndFlagsValue = mediaAndFlags { - if mediaAndFlagsValue.0.first is TelegramMediaStory || mediaAndFlagsValue.0.first is WallpaperPreviewMedia || mediaAndFlagsValue.0.first is UniqueGiftPreviewMedia { + if mediaAndFlagsValue.0.first is TelegramMediaStory || mediaAndFlagsValue.0.first is WallpaperPreviewMedia || mediaAndFlagsValue.0.first is UniqueGiftPreviewMedia || mediaAndFlagsValue.0.first is GiftAuctionPreviewMedia { var flags = mediaAndFlagsValue.1 flags.remove(.preferMediaInline) mediaAndFlags = (mediaAndFlagsValue.0, flags) @@ -394,6 +395,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { contentMediaValue = media } else if media is UniqueGiftPreviewMedia { contentMediaValue = media + } else if media is GiftAuctionPreviewMedia { + contentMediaValue = media } } } @@ -624,6 +627,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { case .link: buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)! cornerIcon = true + case .bid: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconBidIncoming(presentationData.theme.theme)! } } } else { @@ -634,6 +639,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { case .link: buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)! cornerIcon = true + case .bid: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconBidOutgoing(presentationData.theme.theme)! } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 37d39d7082..52ae49c1dc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -280,7 +280,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { - case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy @@ -554,7 +554,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View hasServiceMessage = false } - case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _, _, _): + case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _, _, _, _, _): if case let .generic(gift) = gift { if let releasedBy = gift.releasedBy, let peer = item.message.peers[releasedBy], let addressName = peer.addressName { creatorButtonTitle = item.presentationData.strings.Notification_StarGift_ReleasedBy("**@\(addressName)**").string diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 37ae63c442..bb8e7164d3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1028,6 +1028,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } else if let _ = media as? UniqueGiftPreviewMedia { isGift = true unboundSize = CGSize(width: 200.0, height: 200.0) + } else if let _ = media as? GiftAuctionPreviewMedia { + isGift = true + unboundSize = CGSize(width: 200.0, height: 200.0) } else { var extendedMedia: TelegramExtendedMedia? if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia { @@ -1832,6 +1835,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } else if let _ = media as? UniqueGiftPreviewMedia { updatedStatusSignal = .single((.Local, nil)) + } else if let _ = media as? GiftAuctionPreviewMedia { + updatedStatusSignal = .single((.Local, nil)) } } @@ -2204,6 +2209,38 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr containerSize: imageFrame.size ) + if let giftView = giftView.view { + if giftView.superview == nil { + strongSelf.pinchContainerNode.contentNode.view.addSubview(giftView) + } + giftView.frame = imageFrame + } + } else if let giftPreview = media as? GiftAuctionPreviewMedia, let gift = giftPreview.content { + let giftView: ComponentView + if let current = strongSelf.giftView { + giftView = current + } else { + giftView = ComponentView() + strongSelf.giftView = giftView + } + + let _ = giftView.update( + transition: .immediate, + component: AnyComponent( + GiftItemComponent( + context: context, + theme: presentationData.theme.theme, + strings: presentationData.strings, + subject: .auction(gift: gift, centerColor: giftPreview.centerColor, edgeColor: giftPreview.edgeColor, endTime: giftPreview.endTime), + title: gift.title ?? "", + subtitle: presentationData.strings.Chat_Auction_Gifts(gift.availability?.total ?? 0), + mode: .preview + ) + ), + environment: {}, + containerSize: imageFrame.size + ) + if let giftView = giftView.view { if giftView.superview == nil { strongSelf.pinchContainerNode.contentNode.view.addSubview(giftView) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 274f554c46..1132a9cf69 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -343,6 +343,14 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent break } } + case "telegram_auction": + for attribute in webpage.attributes { + if case let .giftAuction(giftAuction) = attribute, case let .generic(gift) = giftAuction.gift { + let media = GiftAuctionPreviewMedia(content: gift, centerColor: UIColor(rgb: UInt32(bitPattern: giftAuction.centerColor)), edgeColor: UIColor(rgb: UInt32(bitPattern: giftAuction.edgeColor)), endTime: giftAuction.endDate) + mediaAndFlags = ([media], []) + break + } + } default: if var file = mainMedia as? TelegramMediaFile, webpage.type != "telegram_theme" { if webpage.imageIsVideoCover, let image = webpage.image { @@ -480,6 +488,21 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent actionTitle = item.presentationData.strings.Chat_ViewCollection case "telegram_story_album": actionTitle = item.presentationData.strings.Chat_ViewAlbum + case "telegram_auction": + var hasEnded = false + for attribute in webpage.attributes { + if case let .giftAuction(giftAuction) = attribute { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if giftAuction.endDate < currentTime { + hasEnded = true + } + break + } + } + title = item.presentationData.strings.Chat_Auction + text = nil + actionTitle = hasEnded ? item.presentationData.strings.Chat_Auction_ViewResults : item.presentationData.strings.Chat_Auction_Join + actionIcon = !hasEnded ? .bid : nil default: break } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index e356216033..b3e9e0fc54 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1462,6 +1462,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .giftCollection: break + case .auction: + break case .sendGift: break } diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 78fe8a36ad..48dc56d531 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -111,7 +111,12 @@ public final class GiftCompositionComponent: Component { private var animatePreviewTransition = false private var animateBackdropSwipe = false - private enum SpinState { case idle, spinning, decelerating, settled } + private enum SpinState { + case idle + case spinning + case decelerating + case settled + } private var spinState: SpinState = .idle private var spinLink: SharedDisplayLinkDriver.Link? private var lastSpawnTime: CFTimeInterval? @@ -124,18 +129,17 @@ public final class GiftCompositionComponent: Component { private var decelerationStepIndex: Int = 0 private var decelContainer: UIView? private var decelItemHosts: [UIView] = [] - private let decelAnimationKey = "decel.container.move" private var activeWrappers: [UIView] = [] - private struct SpinGeom { + private struct SpinParams { var availableSize: CGSize var iconSize: CGSize var scale: CGFloat var centerX: CGFloat var centerY: CGFloat } - private var spinGeom: SpinGeom? + private var spinGeom: SpinParams? private var spinPool: [StarGift.UniqueGift.Attribute] = [] private var spinPoolIndex: Int = 0 @@ -285,7 +289,7 @@ public final class GiftCompositionComponent: Component { self.spinPoolIndex = 0 let centerY = 88.0 + (self.component?.animationOffset?.y ?? 0.0) - self.spinGeom = SpinGeom( + self.spinGeom = SpinParams( availableSize: availableSize, iconSize: iconSize, scale: scale, @@ -893,7 +897,7 @@ public final class GiftCompositionComponent: Component { ) } else if self.spinState == .spinning { let centerY = 88.0 + (component.animationOffset?.y ?? 0.0) - self.spinGeom = SpinGeom( + self.spinGeom = SpinParams( availableSize: availableSize, iconSize: iconSize, scale: scaleValue, diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD index 1c86016b80..fc62375e5c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent" ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index ac7233667d..aa7de4796a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -17,6 +17,7 @@ import PeerInfoCoverComponent import Markdown import CheckNode import BundleIconComponent +import AnimatedTextComponent public final class GiftItemComponent: Component { public enum Style { @@ -28,6 +29,7 @@ public final class GiftItemComponent: Component { case premium(months: Int32, price: String) case starGift(gift: StarGift.Gift, price: String) case uniqueGift(gift: StarGift.UniqueGift, price: String?) + case auction(gift: StarGift.Gift, centerColor: UIColor, edgeColor: UIColor, endTime: Int32) } public struct Ribbon: Equatable { @@ -141,6 +143,7 @@ public final class GiftItemComponent: Component { case select case buttonIcon case tableIcon + case header } let context: AccountContext @@ -298,6 +301,9 @@ public final class GiftItemComponent: Component { private let button = ComponentView() private let label = ComponentView() private let ton = ComponentView() + + private let badgeText = ComponentView() + private let badgeBackground = ComponentView() private let ribbonOutline = UIImageView() private let ribbon = UIImageView() @@ -321,6 +327,8 @@ public final class GiftItemComponent: Component { private var resellBackground: BlurredBackgroundView? private let reselLabel = ComponentView() + private var giftAuctionTimer: SwiftSignalKit.Timer? + override init(frame: CGRect) { super.init(frame: frame) @@ -348,6 +356,7 @@ public final class GiftItemComponent: Component { deinit { self.disposables.dispose() + self.giftAuctionTimer?.invalidate() } @objc private func buttonPressed() { @@ -408,6 +417,10 @@ public final class GiftItemComponent: Component { size = CGSize(width: 18.0, height: 18.0) iconSize = size cornerRadius = 0.0 + case .header: + size = availableSize + iconSize = CGSize(width: 106.0, height: 106.0) + cornerRadius = 16.0 } var backgroundSize = size if case .grid = component.mode { @@ -453,10 +466,13 @@ public final class GiftItemComponent: Component { var patternFile: TelegramMediaFile? var files: [Int64: TelegramMediaFile] = [:] + var animatedBadgeItems: [AnimatedTextComponent.Item] = [] + var placeholderColor = component.theme.list.mediaPlaceholderColor let emoji: ChatTextInputTextCustomEmojiAttribute? var animationOffset: CGFloat = 0.0 + var explicitAnimationOffset: CGFloat = 0.0 switch component.subject { case let .premium(months, _): emoji = ChatTextInputTextCustomEmojiAttribute( @@ -507,6 +523,48 @@ public final class GiftItemComponent: Component { } else { emoji = nil } + case let .auction(gift, centerColor, edgeColor, endTime): + animationOffset = 16.0 + explicitAnimationOffset = -16.0 + animationFile = gift.file + backgroundColor = edgeColor + secondBackgroundColor = centerColor + + emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: gift.file.fileId.id, + file: gift.file + ) + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let endTimeout = max(0, endTime - currentTime) + + if endTimeout > 0 { + let hours = Int(endTimeout / 3600) + let minutes = Int((endTimeout % 3600) / 60) + let seconds = Int(endTimeout % 60) + + if hours > 0 { + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 1))) + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "colon1", content: .text(":"))) + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":"))) + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } else { + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":"))) + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } + } else { + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "finished", content: .text(component.strings.Chat_Auction_Finished))) + } + + if self.giftAuctionTimer == nil { + self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.componentState?.updated() + }, queue: Queue.mainQueue()) + self.giftAuctionTimer?.start() + } } if [.buttonIcon, .tableIcon].contains(component.mode) { @@ -547,7 +605,7 @@ public final class GiftItemComponent: Component { } } - 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)), size: iconSize) + 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) if let animationLayer = self.animationLayer { animationTransition.setFrame(layer: animationLayer, frame: animationFrame) } @@ -582,7 +640,84 @@ public final class GiftItemComponent: Component { } } - if case .generic = component.mode { + if case .preview = component.mode { + if let title = component.title { + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: size.height - 52.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + } + + if let subtitle = component.subtitle { + let subtitleSize = self.subtitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitle, font: Font.regular(14.0), textColor: .white.withAlphaComponent(0.5))), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: availableSize + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) / 2.0), y: size.height - 33.0), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + } + + if !animatedBadgeItems.isEmpty { + let badgeTextSize = self.badgeText.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + AnimatedTextComponent( + font: Font.with(size: 11.0, weight: .regular, traits: .monospacedNumbers), + color: .white, + items: animatedBadgeItems, + noDelay: true + ) + ), + environment: {}, + containerSize: availableSize + ) + + let badgeBackgroundSize = CGSize(width: badgeTextSize.width + 8.0, height: 18.0) + let _ = self.badgeBackground.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + RoundedRectangle(color: UIColor(white: 0.0, alpha: 0.2), cornerRadius: 9.0) + ), + environment: {}, + containerSize: badgeBackgroundSize + ) + + if let badgeBackgroundView = self.badgeBackground.view, let badgeTextView = self.badgeText.view { + if badgeBackgroundView.superview == nil { + self.addSubview(badgeBackgroundView) + self.addSubview(badgeTextView) + } + badgeTextView.frame = CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: badgeTextSize) + badgeBackgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(badgeTextView.frame.center.x - badgeBackgroundSize.width / 2.0), y: floorToScreenPixels(badgeTextView.frame.center.y - badgeBackgroundSize.height / 2.0)), size: badgeBackgroundSize) + } + } + } else if case .generic = component.mode { if let title = component.title { let titleSize = self.title.update( transition: transition, @@ -631,10 +766,9 @@ public final class GiftItemComponent: Component { let price: String switch component.subject { case let .premium(_, priceValue), let .starGift(_, priceValue): - if case let .starGift(gift, _) = component.subject, gift.flags.contains(.isAuction) { + if case let .starGift(gift, priceValue) = component.subject, gift.flags.contains(.isAuction) { buttonColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a) - //todo:localize - price = "Join" + price = priceValue } else { if priceValue.contains("#") { buttonColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a) @@ -656,6 +790,10 @@ public final class GiftItemComponent: Component { } price = priceValue ?? component.strings.Gift_Options_Gift_Transfer tinted = true + case .auction: + buttonColor = .clear + price = "" + break } let buttonSize = self.button.update( @@ -1160,7 +1298,12 @@ public final class GiftItemComponent: Component { context.translateBy(x: 0.0, y: size.height) context.scaleBy(x: 1.0, y: -1.0) - context.clip(to: CGRect(origin: CGPoint(x: size.width - 58.0, y: 91.0 - UIScreenPixel), size: ribbonOutline.size), mask: cgImage) + var maskY = 91.0 - UIScreenPixel + if size.height < 121.0 { + maskY = 57.0 - UIScreenPixel + } + + context.clip(to: CGRect(origin: CGPoint(x: size.width - 58.0 - UIScreenPixel, y: maskY), size: ribbonOutline.size), mask: cgImage) context.setBlendMode(.clear) context.setFillColor(UIColor.clear.cgColor) context.fill(CGRect(origin: .zero, size: size)) @@ -1172,7 +1315,6 @@ public final class GiftItemComponent: Component { outlineLayer.frame = outlineFrame } - } else if let outlineLayer = self.outlineLayer { self.outlineLayer = nil outlineLayer.removeFromSuperlayer() diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 501c91ec43..5223111f19 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -240,6 +240,8 @@ final class GiftOptionsScreenComponent: Component { private var dismissed = false + private var auctionDisposable = MetaDisposable() + private var chevronImage: (UIImage, PresentationTheme)? private var resaleConfiguration: StarsSubscriptionConfiguration? @@ -274,6 +276,7 @@ final class GiftOptionsScreenComponent: Component { deinit { self.starsStateDisposable?.dispose() + self.auctionDisposable.dispose() } func scrollToTop() { @@ -363,10 +366,58 @@ final class GiftOptionsScreenComponent: Component { } if gift.flags.contains(.isAuction) { -// let giftController = component.context.sharedContext.makeGiftAuctionViewScreen( -// context: component, -// auctionContext: <#T##GiftAuctionContext#> -// ) + guard let giftAuctionsManager = component.context.giftAuctionsManager else { + return + } + self.auctionDisposable.set((giftAuctionsManager.auctionContext(for: .giftId(gift.id)) + |> deliverOnMainQueue).start(next: { [weak self, weak mainController] auctionContext in + guard let auctionContext, let component = self?.component, let mainController else { + return + } + if let currentBidPeerId = auctionContext.currentBidPeerId { + if currentBidPeerId == component.peerId { + let giftController = component.context.sharedContext.makeGiftAuctionBidScreen( + context: component.context, + auctionContext: auctionContext + ) + mainController.push(giftController) + } else { + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: currentBidPeerId)) + |> deliverOnMainQueue).start(next: { [weak self, weak mainController] peer in + guard let component = self?.component, let environment = self?.environment, let mainController else { + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController( + context: component.context, + title: environment.strings.Gift_Auction_Ongoing_Title, + text: auctionContext.currentBidPeerId == component.context.account.peerId ? environment.strings.Gift_Auction_Ongoing_TextYourself : environment.strings.Gift_Auction_Ongoing_Text(peer?.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder) ?? "").string, + actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {}), + TextAlertAction(type: .defaultAction, title: environment.strings.Gift_Auction_Ongoing_View, action: { [weak mainController] in + guard let mainController else { + return + } + let giftController = component.context.sharedContext.makeGiftAuctionBidScreen( + context: component.context, + auctionContext: auctionContext + ) + mainController.push(giftController) + }) + ], + parseMarkdown: true + ) + mainController.present(alertController, in: .window(.root)) + }) + } + } else { + let giftController = component.context.sharedContext.makeGiftAuctionViewScreen( + context: component.context, + auctionContext: auctionContext + ) + mainController.push(giftController) + } + })) } else { if let availability = gift.availability, availability.remains == 0 { if availability.resale > 0 { @@ -529,8 +580,7 @@ final class GiftOptionsScreenComponent: Component { let text: String var ribbonColor: GiftItemComponent.Ribbon.Color = .blue if gift.flags.contains(.isAuction) { - //TODO:localize - text = "auction" + text = environment.strings.Gift_Options_Gift_Auction ribbonColor = .orange outline = .orange } else if let perUserLimit = gift.perUserLimit { @@ -546,8 +596,7 @@ final class GiftOptionsScreenComponent: Component { if !isSoldOut && gift.flags.contains(.requiresPremium) { let text: String if gift.flags.contains(.isAuction) { - //TODO:localize - text = "auction" + text = environment.strings.Gift_Options_Gift_Auction } else if component.context.isPremium, let perUserLimit = gift.perUserLimit { text = environment.strings.Gift_Options_Gift_Premium_Left(perUserLimit.remains) } else { @@ -582,7 +631,9 @@ final class GiftOptionsScreenComponent: Component { let subject: GiftItemComponent.Subject switch gift { case let .generic(gift): - if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars { + if gift.flags.contains(.isAuction) { + subject = .starGift(gift: gift, price: "Join") + } else if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars { let priceString = presentationStringsFormattedNumber(Int32(minResaleStars), environment.dateTimeFormat.groupingSeparator) if let resaleConfiguration = self.resaleConfiguration, minResaleStars == resaleConfiguration.starGiftResaleMaxStarsAmount || availability.resale == 1 { subject = .starGift(gift: gift, price: "# \(priceString)") diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index e91bbd2990..5ae83ff711 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -245,7 +245,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { case let .starGift(gift): media = [ TelegramMediaAction( - action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil, giftMessageId: nil, upgradeSeparate: false) + action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil, giftMessageId: nil, upgradeSeparate: false, isAuctionAcquired: false, toPeerId: nil) ) ] } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift index f0124eb903..9ad6628308 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift @@ -241,6 +241,40 @@ private final class GiftAuctionAcquiredScreenComponent: Component { } self.bottomEdgeEffectView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } + + private func openPeer(_ peer: EnginePeer, dismiss: Bool = true) { + guard let component = self.component, let controller = self.environment?.controller() as? GiftAuctionAcquiredScreen, let navigationController = controller.navigationController as? NavigationController else { + return + } + + let context = component.context + let action = { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + chatController: nil, + context: context, + chatLocation: .peer(peer), + subject: nil, + botStart: nil, + updateTextInputState: nil, + keepStack: .always, + useExisting: true, + purposefulAction: nil, + scrollToEndIfExists: false, + activateMessageSearch: nil, + animated: true + )) + } + + if dismiss { + controller.dismiss() + Queue.mainQueue().after(0.4, { + action() + }) + } else { + action() + } + } func update(component: GiftAuctionAcquiredScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true @@ -268,15 +302,6 @@ private final class GiftAuctionAcquiredScreenComponent: Component { if themeUpdated { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor - - var locations: [NSNumber] = [] - var colors: [CGColor] = [] - let numStops = 6 - for i in 0 ..< numStops { - let step = CGFloat(i) / CGFloat(numStops - 1) - locations.append(step as NSNumber) - colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor) - } } transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) @@ -325,7 +350,7 @@ private final class GiftAuctionAcquiredScreenComponent: Component { AnyComponentWithIdentity( id: "title", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Round #\(gift.round)", font: tableBoldFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Acquired_Round("\(gift.round)").string, font: tableBoldFont, textColor: tableTextColor))) ) ) ], spacing: 1.0)) @@ -334,7 +359,7 @@ private final class GiftAuctionAcquiredScreenComponent: Component { items.append(.init( id: "recipient", - title: "Recipient", + title: environment.strings.Gift_Acquired_Recipient, component: AnyComponent(Button( content: AnyComponent( PeerCellComponent( @@ -344,15 +369,18 @@ private final class GiftAuctionAcquiredScreenComponent: Component { peer: gift.peer ) ), - action: { - + action: { [weak self] in + guard let self else { + return + } + self.openPeer(gift.peer, dismiss: false) } )) )) items.append(.init( id: "date", - title: "Date", + title: environment.strings.Gift_Acquired_Date, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: gift.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat), font: tableFont, textColor: tableTextColor)))) )) @@ -366,7 +394,7 @@ private final class GiftAuctionAcquiredScreenComponent: Component { items.append(.init( id: "bid", - title: "Accepted Bid", + title: environment.strings.Gift_Acquired_AcceptedBid, component: AnyComponent(HStack([ AnyComponentWithIdentity(id: "stars", component: AnyComponent(MultilineTextWithEntitiesComponent( context: component.context, @@ -381,11 +409,10 @@ private final class GiftAuctionAcquiredScreenComponent: Component { component: AnyComponent(Button( content: AnyComponent(ButtonContentComponent( context: component.context, - text: "TOP \(gift.position)", + text: environment.strings.Gift_Acquired_Top("\(gift.position)").string, color: environment.theme.list.itemAccentColor )), action: { - } )) ) @@ -463,13 +490,7 @@ private final class GiftAuctionAcquiredScreenComponent: Component { let title = self.title let actionButton = self.actionButton - let titleText: String - if component.acquiredGifts.count == 1 { - titleText = "1 Item Bought" - } else { - titleText = "\(component.acquiredGifts.count) Items Bought" - } - + let titleText = environment.strings.Gift_Acquired_Title(Int32(component.acquiredGifts.count)) let titleSize = title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -617,14 +638,6 @@ private final class GiftAuctionAcquiredScreenComponent: Component { } public class GiftAuctionAcquiredScreen: ViewControllerComponentContainer { - public final class TransitionOut { - public let sourceView: UIView - - init(sourceView: UIView) { - self.sourceView = sourceView - } - } - private let context: AccountContext private var didPlayAppearAnimation: Bool = false diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift new file mode 100644 index 0000000000..e69cdba807 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift @@ -0,0 +1,774 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import ComponentFlow +import AccountContext +import ViewControllerComponent +import TelegramCore +import SwiftSignalKit +import Display +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import ButtonComponent +import PlainButtonComponent +import Markdown +import BundleIconComponent +import TextFormat +import TelegramStringFormatting +import GlassBarButtonComponent +import GiftItemComponent +import EdgeEffect +import AnimatedTextComponent + +private final class GiftAuctionActiveBidsScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: GiftAuctionActiveBidsScreenComponent, rhs: GiftAuctionActiveBidsScreenComponent) -> Bool { + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var containerCornerRadius: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.containerCornerRadius = containerCornerRadius + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let containerView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let topEdgeEffectView: EdgeEffectView + + private let backgroundHandleView: UIImageView + + private let closeButton = ComponentView() + private let title = ComponentView() + private var itemsViews: [Int64: ComponentView] = [:] + + private var auctionStates: [GiftAuctionContext.State] = [] + private var auctionStatesDisposable: Disposable? + + private var ignoreScrolling: Bool = false + + private var giftAuctionTimer: SwiftSignalKit.Timer? + + private var component: GiftAuctionActiveBidsScreenComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var environment: ViewControllerComponentContainer.Environment? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.dimView = UIView() + self.containerView = UIView() + + self.containerView.clipsToBounds = true + self.containerView.layer.cornerRadius = 40.0 + self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 40.0 + + self.backgroundHandleView = UIImageView() + + self.navigationBarContainer = SparseContainerView() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + self.topEdgeEffectView = EdgeEffectView() + self.topEdgeEffectView.clipsToBounds = true + self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.topEdgeEffectView.layer.cornerRadius = 40.0 + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.addSubview(self.containerView) + self.containerView.layer.addSublayer(self.backgroundLayer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.containerView.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.containerView.addSubview(self.navigationBarContainer) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.auctionStatesDisposable?.dispose() + self.giftAuctionTimer?.invalidate() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func updateScrolling(transition: ComponentTransition) { + guard let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + var topOffsetFraction = self.scrollView.bounds.minY / 100.0 + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + 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 + + let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction + let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) + let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction + + var containerTransform = CATransform3DIdentity + containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) + containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) + transition.setTransform(view: self.containerView, transform: containerTransform) + transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + + func update(component: GiftAuctionActiveBidsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let fillingSize: CGFloat + if case .regular = environment.metrics.widthClass { + fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0 + } else { + fillingSize = min(availableSize.width, 428.0) - environment.safeInsets.left * 2.0 + } + let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 16.0 + + if self.component == nil, let giftAuctionsManager = component.context.giftAuctionsManager { + self.auctionStatesDisposable = (giftAuctionsManager.state + |> deliverOnMainQueue).start(next: { [weak self] auctionStates in + guard let self else { + return + } + self.auctionStates = auctionStates + self.state?.updated(transition: .immediate) + }) + + self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.state?.updated() + }, queue: Queue.mainQueue()) + self.giftAuctionTimer?.start() + } + + self.component = component + self.state = state + self.environment = environment + + let theme = environment.theme.withModalBlocksBackground() + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = theme.list.blocksBackgroundColor.cgColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + var contentHeight: CGFloat = 75.0 + for auctionState in self.auctionStates { + let id = auctionState.gift.giftId + let itemView: ComponentView + if let current = self.itemsViews[id] { + itemView = current + } else { + itemView = ComponentView() + self.itemsViews[id] = itemView + } + + let itemSize = itemView.update( + transition: transition, + component: AnyComponent( + ActiveAuctionComponent( + context: component.context, + theme: theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + state: auctionState, + currentTime: currentTime, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + if let giftAuctionsManager = component.context.giftAuctionsManager { + let _ = (giftAuctionsManager.auctionContext(for: .giftId(id)) + |> deliverOnMainQueue).start(next: { [weak self] auction in + guard let self, let component = self.component, let auction, let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + controller.dismiss() + + let bidController = component.context.sharedContext.makeGiftAuctionBidScreen(context: component.context, auctionContext: auction) + 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 + + if self.backgroundHandleView.image == nil { + self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) + } + self.backgroundHandleView.tintColor = environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(environment.theme.overallDarkAppearance ? 0.2 : 0.07) + let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0)) + if self.backgroundHandleView.superview == nil { + self.navigationBarContainer.addSubview(self.backgroundHandleView) + } + transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame) + + let closeButtonSize = self.closeButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: environment.theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { [weak self] _ in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.navigationBarContainer.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + contentHeight += environment.safeInsets.bottom + + var initialContentHeight = contentHeight + let clippingY: CGFloat + + let titleText: String = environment.strings.Gift_ActiveAuctions_Title(Int32(self.auctionStates.count)) + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + 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: 26.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.navigationBarContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + initialContentHeight = contentHeight + + let edgeEffectHeight: CGFloat = 80.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) + transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) + self.topEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) + if self.topEdgeEffectView.superview == nil { + self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0) + } + + clippingY = availableSize.height + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 38.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height))) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { + let bottomInset: CGFloat = contentHeight - 12.0 + + let layout = ContainerViewLayout( + size: 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: 0.0, bottom: 0.0, right: 0.0), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class GiftAuctionActiveBidsScreen: ViewControllerComponentContainer { + private let context: AccountContext + + private var didPlayAppearAnimation: Bool = false + private var isDismissed: Bool = false + + public init(context: AccountContext) { + self.context = context + + super.init(context: context, component: GiftAuctionActiveBidsScreenComponent( + context: context + ), navigationBarAppearance: .none, theme: .default) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + + if let componentView = self.node.hostView.componentView as? GiftAuctionActiveBidsScreenComponent.View { + componentView.animateIn() + } + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? GiftAuctionActiveBidsScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} + +private final class ActiveAuctionComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat + let state: GiftAuctionContext.State + let currentTime: Int32 + let action: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, + state: GiftAuctionContext.State, + currentTime: Int32, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.dateTimeFormat = dateTimeFormat + self.state = state + self.currentTime = currentTime + self.action = action + } + + static func ==(lhs: ActiveAuctionComponent, rhs: ActiveAuctionComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.state != rhs.state { + return false + } + if lhs.currentTime != rhs.currentTime { + return false + } + return true + } + + final class View: UIView { + private let icon = ComponentView() + private let title = ComponentView() + private let subtitle = ComponentView() + private let button = ComponentView() + + private var component: ActiveAuctionComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.cornerRadius = 26.0 + self.clipsToBounds = true + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(component: ActiveAuctionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + self.backgroundColor = component.theme.list.itemBlocksBackgroundColor + + var size = CGSize(width: availableSize.width, height: 0.0) + size.height += 11.0 + + + if case let .generic(gift) = component.state.gift { + let titleSize = self.icon.update( + transition: .immediate, + component: AnyComponent(GiftItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + subject: .starGift(gift: gift, price: ""), + mode: .preview + )), + environment: {}, + containerSize: CGSize(width: 64.0, height: 64.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: 2.0, y: 0.0), size: titleSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + } + + var endTime = component.currentTime + + 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 { + 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 { + subtitleText = component.strings.Gift_ActiveAuctions_Outbid(bidString).string + subtitleTextColor = component.theme.list.itemDestructiveColor + } else { + subtitleText = component.strings.Gift_ActiveAuctions_Winning(bidString, "\(component.state.place ?? 0)").string + } + + endTime = nextRoundDate + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), + maximumNumberOfLines: 2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 63.0 - 20.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: 63.0, y: size.height), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + size.height += titleSize.height + size.height += 2.0 + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = subtitleTextColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(subtitleText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: "#") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedString.string)) + } + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextWithEntitiesComponent(context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: .clear, text: .plain(attributedString), maximumNumberOfLines: 2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 63.0 - 20.0, height: 100.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: 63.0, y: size.height), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + subtitleView.frame = subtitleFrame + } + size.height += subtitleSize.height + size.height += 19.0 + + let endTimeout = max(0, endTime - component.currentTime) + let hours = Int(endTimeout / 3600) + let minutes = Int((endTimeout % 3600) / 60) + let seconds = Int(endTimeout % 60) + + var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] + if hours > 0 { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 1))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon1", content: .text(":"))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":"))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } else { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":"))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } + + let buttonSize = self.button.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity(id: "label", component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + BundleIconComponent(name: "Premium/Auction/BidSmall", tintColor: component.theme.list.itemCheckColors.foregroundColor) + )), + AnyComponentWithIdentity(id: "label", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: component.strings.Gift_ActiveAuctions_RaiseBid, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor))) + )), + AnyComponentWithIdentity(id: "timer", component: AnyComponent( + AnimatedTextComponent( + font: Font.with(size: 17.0, weight: .medium, traits: .monospacedNumbers), + color: component.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), + items: buttonAnimatedTitleItems, + noDelay: true + ) + )) + ], spacing: 5.0) + )), + action: { + component.action() + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 32.0, height: 52.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: size.height), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + buttonView.frame = buttonFrame + } + size.height += buttonSize.height + size.height += 16.0 + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index b87ee4f7c3..74e42f0449 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -28,6 +28,7 @@ import AnimatedTextComponent import BotPaymentsUI import UndoUI import GiftItemComponent +import LottieComponent private final class BadgeComponent: Component { let theme: PresentationTheme @@ -78,7 +79,8 @@ private final class BadgeComponent: Component { final class View: UIView { private let badgeView: UIView private let badgeMaskView: UIView - private let badgeShapeLayer = SimpleShapeLayer() + private let badgeShapeView: UIImageView + private let badgeShapeAnimation = ComponentView() private let badgeForeground: SimpleLayer let badgeIcon: UIImageView @@ -99,12 +101,12 @@ private final class BadgeComponent: Component { override init(frame: CGRect) { self.badgeView = UIView() self.badgeView.alpha = 0.0 + self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0) - self.badgeShapeLayer.fillColor = UIColor.white.cgColor - self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + self.badgeShapeView = UIImageView() self.badgeMaskView = UIView() - self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) + self.badgeMaskView.addSubview(self.badgeShapeView) self.badgeView.mask = self.badgeMaskView self.badgeForeground = SimpleLayer() @@ -176,7 +178,6 @@ private final class BadgeComponent: Component { let countWidth: CGFloat = badgeLabelSize.width + 3.0 var badgeWidth: CGFloat = countWidth + 54.0 - var badgeOffset: CGPoint = .zero if let prefix = component.prefix { let prefixSize = self.prefix.update( @@ -217,14 +218,12 @@ private final class BadgeComponent: Component { subtitleView.alpha = 0.0 } - - let badgeSize = CGSize(width: badgeWidth, height: 48.0) let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0) self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) - self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize) self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize) + self.badgeView.center = CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height) self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 600.0, height: badgeFullSize.height + 10.0)) @@ -233,7 +232,6 @@ private final class BadgeComponent: Component { self.badgeView.alpha = 1.0 - let size = badgeSize transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: badgeOffset.x + 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0 + badgeOffset.y), size: badgeLabelSize)) @@ -261,17 +259,55 @@ private final class BadgeComponent: Component { return size } - func adjustTail(size: CGSize, overflowWidth: CGFloat) { - var tailPosition = size.width * 0.5 - tailPosition += overflowWidth - tailPosition = max(0.0, min(size.width, tailPosition)) + func adjustTail(size: CGSize, tailOffset: CGFloat, transition: ComponentTransition) { + if self.badgeShapeView.image == nil { + self.badgeShapeView.image = generateStretchableFilledCircleImage(diameter: 48.0, color: UIColor.white) + } + self.badgeShapeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 48.0)) - let tailPositionFraction = tailPosition / size.width - self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: size, tailPosition: tailPositionFraction).cgPath - - let transition: ContainedViewLayoutTransition = .immediate - transition.updateAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: tailPositionFraction, y: 1.0)) - transition.updatePosition(layer: self.badgeView.layer, position: CGPoint(x: (tailPositionFraction - 0.5) * size.width, y: 0.0)) + let badgeShapeSize = CGSize(width: 78, height: 60) + let _ = self.badgeShapeAnimation.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "badge_with_tail"), + color: .white, + placeholderColor: nil, + startingPosition: .begin, + size: badgeShapeSize, + renderingScale: UIScreenScale, + loop: false, + playOnce: nil + )), + environment: {}, + containerSize: badgeShapeSize + ) + if let badgeShapeAnimationView = self.badgeShapeAnimation.view as? LottieComponent.View { + if badgeShapeAnimationView.superview == nil { + badgeShapeAnimationView.layer.anchorPoint = CGPoint() + self.badgeMaskView.addSubview(badgeShapeAnimationView) + } + + var shapeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeShapeSize) + + let badgeShapeWidth = badgeShapeSize.width + + let midFrame = 359 / 2 + if tailOffset < badgeShapeWidth * 0.5 { + let frameIndex = Int(floor(CGFloat(midFrame) * tailOffset / (badgeShapeWidth * 0.5))) + badgeShapeAnimationView.setFrameIndex(index: frameIndex) + } else if tailOffset >= size.width - badgeShapeWidth * 0.5 { + let endOffset = tailOffset - (size.width - badgeShapeWidth * 0.5) + let frameIndex = midFrame + Int(floor(CGFloat(359 - midFrame) * endOffset / (badgeShapeWidth * 0.5))) + badgeShapeAnimationView.setFrameIndex(index: frameIndex) + shapeFrame.origin.x = size.width - badgeShapeWidth + } else { + badgeShapeAnimationView.setFrameIndex(index: midFrame) + shapeFrame.origin.x = tailOffset - badgeShapeWidth * 0.5 + } + + badgeShapeAnimationView.center = shapeFrame.origin + badgeShapeAnimationView.bounds = CGRect(origin: CGPoint(), size: shapeFrame.size) + } } func updateBadgeAngle(angle: CGFloat) { @@ -323,15 +359,18 @@ private final class BadgeComponent: Component { private final class PeerPlaceComponent: Component { let theme: PresentationTheme + let color: UIColor let place: Int32 let groupingSeparator: String init( theme: PresentationTheme, + color: UIColor, place: Int32, groupingSeparator: String ) { self.theme = theme + self.color = color self.place = place self.groupingSeparator = groupingSeparator } @@ -340,6 +379,9 @@ private final class PeerPlaceComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.color != rhs.color { + return false + } if lhs.place != rhs.place { return false } @@ -383,7 +425,7 @@ private final class PeerPlaceComponent: Component { backgroundColors = nil } } else { - textColor = component.theme.list.itemSecondaryTextColor + textColor = component.color backgroundColors = nil } @@ -430,12 +472,18 @@ private final class PeerPlaceComponent: Component { } private final class PeerComponent: Component { + enum Status { + case winning + case outbid + } + let context: AccountContext let theme: PresentationTheme let groupingSeparator: String let peer: EnginePeer let place: Int32 let amount: Int64 + let status: Status? let isLast: Bool init( @@ -445,6 +493,7 @@ private final class PeerComponent: Component { peer: EnginePeer, place: Int32, amount: Int64, + status: Status? = nil, isLast: Bool ) { self.context = context @@ -453,6 +502,7 @@ private final class PeerComponent: Component { self.peer = peer self.place = place self.amount = amount + self.status = status self.isLast = isLast } @@ -472,6 +522,9 @@ private final class PeerComponent: Component { if lhs.amount != rhs.amount { return false } + if lhs.status != rhs.status { + return false + } if lhs.isLast != rhs.isLast { return false } @@ -506,19 +559,29 @@ private final class PeerComponent: Component { let size = CGSize(width: availableSize.width, height: 52.0) + var color = component.theme.list.itemSecondaryTextColor + switch component.status { + case .winning: + color = component.theme.list.itemDisclosureActions.constructive.fillColor + case .outbid: + color = component.theme.list.itemDestructiveColor + default: + break + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let badgeSize = self.place.update( + let placeSize = self.place.update( transition: .immediate, - component: AnyComponent(PeerPlaceComponent(theme: component.theme, place: component.place, groupingSeparator: presentationData.dateTimeFormat.groupingSeparator)), + component: AnyComponent(PeerPlaceComponent(theme: component.theme, color: color, place: component.place, groupingSeparator: presentationData.dateTimeFormat.groupingSeparator)), environment: {}, containerSize: CGSize(width: 40.0, height: 40.0) ) - let badgeFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - badgeSize.height) / 2.0)), size: badgeSize) - if let badgeView = self.place.view { - if badgeView.superview == nil { - self.addSubview(badgeView) + let placeFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - placeSize.height) / 2.0)), size: placeSize) + if let placeView = self.place.view { + if placeView.superview == nil { + self.addSubview(placeView) } - badgeView.frame = badgeFrame + placeView.frame = placeFrame } let avatarNode: AvatarNode @@ -542,7 +605,7 @@ private final class PeerComponent: Component { text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, - containerSize: CGSize(width: avatarSize.width + 10.0 * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - 120.0 - 110.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: 110.0, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) if let titleView = self.title.view { @@ -555,7 +618,7 @@ private final class PeerComponent: Component { let amountSize = self.amount.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(Int32(clamping: component.amount), component.groupingSeparator), font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)) + text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(Int32(clamping: component.amount), component.groupingSeparator), font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: component.theme.list.itemSecondaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) @@ -594,6 +657,7 @@ private final class SliderBackgroundComponent: Component { let strings: PresentationStrings let value: CGFloat let topCutoff: CGFloat? + let giftsPerRound: Int32 let color: UIColor init( @@ -601,12 +665,14 @@ private final class SliderBackgroundComponent: Component { strings: PresentationStrings, value: CGFloat, topCutoff: CGFloat?, + giftsPerRound: Int32, color: UIColor ) { self.theme = theme self.strings = strings self.value = value self.topCutoff = topCutoff + self.giftsPerRound = giftsPerRound self.color = color } @@ -623,6 +689,9 @@ private final class SliderBackgroundComponent: Component { if lhs.topCutoff != rhs.topCutoff { return false } + if lhs.giftsPerRound != rhs.giftsPerRound { + return false + } if lhs.color != rhs.color { return false } @@ -741,10 +810,11 @@ private final class SliderBackgroundComponent: Component { topLineFrameTransition.setFrame(layer: self.topBackgroundLine, frame: topLineFrame) topLineAlphaTransition.setAlpha(layer: self.topBackgroundLine, alpha: topLineAlpha) + //TODO:localize let topTextSize = self.topForegroundText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.strings.SendStarReactions_SliderTop, font: Font.semibold(15.0), textColor: UIColor(white: 1.0, alpha: 0.4))) + text: .plain(NSAttributedString(string: "TOP \(component.giftsPerRound)", font: Font.semibold(15.0), textColor: UIColor(white: 1.0, alpha: 0.4))) )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) @@ -752,7 +822,7 @@ private final class SliderBackgroundComponent: Component { let _ = self.topBackgroundText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.strings.SendStarReactions_SliderTop, font: Font.semibold(15.0), textColor: component.theme.overallDarkAppearance ? UIColor(white: 1.0, alpha: 0.22) : UIColor(white: 0.0, alpha: 0.2))) + text: .plain(NSAttributedString(string: "TOP \(component.giftsPerRound)", font: Font.semibold(15.0), textColor: component.theme.overallDarkAppearance ? UIColor(white: 1.0, alpha: 0.22) : UIColor(white: 0.0, alpha: 0.2))) )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) @@ -872,7 +942,8 @@ private final class GiftAuctionBidScreenComponent: Component { private struct Amount: Equatable { private let sliderSteps: [Int] private let minRealValue: Int - private let maxRealValue: Int + let minAllowedRealValue: Int + let maxRealValue: Int let maxSliderValue: Int private let isLogarithmic: Bool @@ -881,7 +952,7 @@ private final class GiftAuctionBidScreenComponent: Component { private static func makeSliderSteps(minRealValue: Int, maxRealValue: Int, isLogarithmic: Bool) -> [Int] { if isLogarithmic { - var sliderSteps: [Int] = [1, 10, 50, 100, 500, 1_000, 2_000, 5_000, 7_500, 10_000 ] + var sliderSteps: [Int] = [1, 10, 50, 100, 500, 1_000, 2_000, 5_000, 7_500, 10_000, 20_000, 30_000] sliderSteps.removeAll(where: { $0 <= minRealValue }) sliderSteps.insert(minRealValue, at: 0) sliderSteps.removeAll(where: { $0 >= maxRealValue }) @@ -892,9 +963,11 @@ private final class GiftAuctionBidScreenComponent: Component { } } - private static func remapValueToSlider(realValue: Int, maxSliderValue: Int, steps: [Int]) -> Int { + private static func remapValueToSlider(realValue: Int, minAllowedRealValue: Int, maxSliderValue: Int, steps: [Int]) -> Int { guard realValue >= steps.first!, realValue <= steps.last! else { return 0 } + let realValue = max(minAllowedRealValue, realValue) + for i in 0 ..< steps.count - 1 { if realValue >= steps[i] && realValue <= steps[i + 1] { let range = steps[i + 1] - steps[i] @@ -906,7 +979,7 @@ private final class GiftAuctionBidScreenComponent: Component { return maxSliderValue // Return max slider position if value equals the last step } - private static func remapSliderToValue(sliderValue: Int, maxSliderValue: Int, steps: [Int]) -> Int { + private static func remapSliderToValue(sliderValue: Int, minAllowedRealValue: Int, maxSliderValue: Int, steps: [Int]) -> Int { guard sliderValue >= 0, sliderValue <= maxSliderValue else { return steps.first! } let stepIndex = Int(Float(sliderValue) / Float(maxSliderValue) * Float(steps.count - 1)) @@ -916,38 +989,44 @@ private final class GiftAuctionBidScreenComponent: Component { return steps.last! } else { let range = steps[stepIndex + 1] - steps[stepIndex] - return steps[stepIndex] + Int(fraction * Float(range)) + return max(minAllowedRealValue, steps[stepIndex] + Int(fraction * Float(range))) } } - init(realValue: Int, minRealValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) { + init(realValue: Int, minRealValue: Int, minAllowedRealValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) { self.sliderSteps = Amount.makeSliderSteps(minRealValue: minRealValue, maxRealValue: maxRealValue, isLogarithmic: isLogarithmic) self.minRealValue = minRealValue + self.minAllowedRealValue = minAllowedRealValue self.maxRealValue = maxRealValue self.maxSliderValue = maxSliderValue self.isLogarithmic = isLogarithmic self.realValue = realValue - self.sliderValue = Amount.remapValueToSlider(realValue: self.realValue, maxSliderValue: self.maxSliderValue, steps: self.sliderSteps) + self.sliderValue = Amount.remapValueToSlider(realValue: self.realValue, minAllowedRealValue: self.minAllowedRealValue, maxSliderValue: self.maxSliderValue, steps: self.sliderSteps) } - init(sliderValue: Int, minRealValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) { + init(sliderValue: Int, minRealValue: Int, minAllowedRealValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) { self.sliderSteps = Amount.makeSliderSteps(minRealValue: minRealValue, maxRealValue: maxRealValue, isLogarithmic: isLogarithmic) self.minRealValue = minRealValue + self.minAllowedRealValue = minAllowedRealValue self.maxRealValue = maxRealValue self.maxSliderValue = maxSliderValue self.isLogarithmic = isLogarithmic self.sliderValue = sliderValue - self.realValue = Amount.remapSliderToValue(sliderValue: self.sliderValue, maxSliderValue: self.maxSliderValue, steps: self.sliderSteps) + self.realValue = Amount.remapSliderToValue(sliderValue: self.sliderValue, minAllowedRealValue: self.minAllowedRealValue, maxSliderValue: self.maxSliderValue, steps: self.sliderSteps) } func withRealValue(_ realValue: Int) -> Amount { - return Amount(realValue: realValue, minRealValue: self.minRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic) + return Amount(realValue: realValue, minRealValue: self.minRealValue, minAllowedRealValue: self.minAllowedRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic) } func withSliderValue(_ sliderValue: Int) -> Amount { - return Amount(sliderValue: sliderValue, minRealValue: self.minRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic) + return Amount(sliderValue: sliderValue, minRealValue: self.minRealValue, minAllowedRealValue: self.minAllowedRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic) + } + + func withMinAllowedRealValue(_ minAllowedRealValue: Int) -> Amount { + return Amount(realValue: self.realValue, minRealValue: self.minRealValue, minAllowedRealValue: minAllowedRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic) } } @@ -966,20 +1045,20 @@ private final class GiftAuctionBidScreenComponent: Component { private let backgroundHandleView: UIImageView private let closeButton = ComponentView() - private let infoButton = ComponentView() + private let moreButton = ComponentView() + private let moreButtonPlayOnce = ActionSlot() private let title = ComponentView() private let badgeStars = BadgeStarsView() private let sliderBackground = ComponentView() private let slider = ComponentView() + private let sliderPlus = ComponentView() private let badge = ComponentView() private var liveStreamPerks: [ComponentView] = [] private var liveStreamMessagePreview: ComponentView? - - private let myGifts = ComponentView() - + private var myPeerTitle: ComponentView? private var myPeerItem: ComponentView? @@ -991,9 +1070,6 @@ private final class GiftAuctionBidScreenComponent: Component { private var giftAuctionTimer: SwiftSignalKit.Timer? private var peersMap: [EnginePeer.Id: EnginePeer] = [:] - private var giftAuctionAcquiredGifts: [GiftAuctionAcquiredGift] = [] - private var giftAuctionAcquiredGiftsDisposable: Disposable? - private let actionButton = ComponentView() private var ignoreScrolling: Bool = false @@ -1006,7 +1082,7 @@ private final class GiftAuctionBidScreenComponent: Component { private var balance: StarsAmount? - private var amount: Amount = Amount(realValue: 1, minRealValue: 1, maxRealValue: 1000, maxSliderValue: 1000, isLogarithmic: true) + private var amount: Amount = Amount(realValue: 1, minRealValue: 1, minAllowedRealValue: 1, maxRealValue: 1000, maxSliderValue: 1000, isLogarithmic: true) private var didChangeAmount: Bool = false private var cachedStarImage: (UIImage, PresentationTheme)? @@ -1104,7 +1180,6 @@ private final class GiftAuctionBidScreenComponent: Component { deinit { self.balanceDisposable?.dispose() self.giftAuctionDisposable?.dispose() - self.giftAuctionAcquiredGiftsDisposable?.dispose() self.giftAuctionTimer?.invalidate() } @@ -1298,6 +1373,7 @@ private final class GiftAuctionBidScreenComponent: Component { } if let myBidAmount = self.giftAuctionState?.myState.bidAmount, let myMinBidAmount = self.giftAuctionState?.myState.minBidAmount, value < myMinBidAmount { + HapticFeedback().error() let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } controller.present( UndoOverlayController( @@ -1354,6 +1430,9 @@ private final class GiftAuctionBidScreenComponent: Component { self.isLoading = false self.state?.updated() + self.amount = self.amount.withMinAllowedRealValue(Int(value)) + + //TODO:localize let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } controller?.present( UndoOverlayController( @@ -1369,6 +1448,163 @@ private final class GiftAuctionBidScreenComponent: Component { }) } + private func openPeer(_ peer: EnginePeer, dismiss: Bool = true) { + guard let component = self.component, let controller = self.environment?.controller() as? GiftAuctionBidScreen, let navigationController = controller.navigationController as? NavigationController else { + return + } + + let context = component.context + let action = { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + chatController: nil, + context: context, + chatLocation: .peer(peer), + subject: nil, + botStart: nil, + updateTextInputState: nil, + keepStack: .always, + useExisting: true, + purposefulAction: nil, + scrollToEndIfExists: false, + activateMessageSearch: nil, + animated: true + )) + } + + if dismiss { + controller.dismiss() + Queue.mainQueue().after(0.4, { + action() + }) + } else { + action() + } + } + + func share() { + guard let component = self.component, let controller = self.environment?.controller() else { + return + } + + let context = component.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var link = "" + if case let .generic(gift) = component.auctionContext.gift, let slug = gift.auctionSlug { + link = "https://t.me/auction/\(slug)" + } + + let shareController = context.sharedContext.makeShareController( + context: context, + subject: .url(link), + forceExternal: false, + shareStory: nil, + enqueued: { [weak self, weak controller] peerIds, _ in + guard let self else { + return + } + let _ = (context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in + guard let self else { + return + } + let peers = peerList.compactMap { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in + if let self, savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in + guard let peer else { + return + } + self?.openPeer(peer) + Queue.mainQueue().after(0.6) { + controller?.dismiss(animated: false, completion: nil) + } + }) + } + return false + }, additionalView: nil), in: .current) + }) + }, + actionCompleted: { [weak controller] in + controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + ) + controller.present(shareController, in: .window(.root)) + } + + func morePressed(view: UIView, gesture: ContextGesture?) { + guard let component = self.component, let controller = self.environment?.controller() else { + return + } + + let context = component.context + let gift = component.auctionContext.gift + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var link = "" + if case let .generic(gift) = gift, let slug = gift.auctionSlug { + link = "https://t.me/auction/\(slug)" + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Auction_Context_About, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, f in + f(.default) + + let infoController = context.sharedContext.makeGiftAuctionInfoScreen(context: context, gift: gift, completion: nil) + controller?.push(infoController) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Auction_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, f in + f(.default) + + UIPasteboard.general.string = link + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Auction_Context_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in + f(.default) + + self?.share() + }))) + + let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceView: view)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + controller.presentInGlobalOverlay(contextController) + } + func update(component: GiftAuctionBidScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -1460,7 +1696,7 @@ private final class GiftAuctionBidScreenComponent: Component { self.giftAuctionState = state var peerIds: [EnginePeer.Id] = [] - if case let .ongoing(_, _, _, topBidders, _, _, _, _) = state?.auctionState { + if case let .ongoing(_, _, _, _, _, topBidders, _, _, _, _) = state?.auctionState { for bidder in topBidders { if self.peersMap[bidder] == nil { peerIds.append(bidder) @@ -1474,14 +1710,20 @@ private final class GiftAuctionBidScreenComponent: Component { peerIds.append(context.account.peerId) var minBidAmount: Int64 = 100 - if case let .ongoing(_, auctionMinBidAmount, _, _, _, _, _, _) = state?.auctionState { + if case let .ongoing(_, _, _, auctionMinBidAmount, _, _, _, _, _, _) = state?.auctionState { minBidAmount = auctionMinBidAmount } var currentValue = max(Int(minBidAmount), 100) if let myBidAmount = state?.myState.bidAmount { currentValue = Int(myBidAmount) } - self.amount = Amount(realValue: currentValue, minRealValue: Int(minBidAmount), maxRealValue: 30000, maxSliderValue: 999, isLogarithmic: true) + + var minAllowedRealValue: Int64 = minBidAmount + if let myBidAmount = state?.myState.bidAmount { + minAllowedRealValue = myBidAmount + } + + self.amount = Amount(realValue: currentValue, minRealValue: Int(minBidAmount), minAllowedRealValue: Int(minAllowedRealValue), maxRealValue: 30000, maxSliderValue: 999, isLogarithmic: true) transition = .immediate } @@ -1512,17 +1754,6 @@ private final class GiftAuctionBidScreenComponent: Component { self?.state?.updated() }, queue: Queue.mainQueue()) self.giftAuctionTimer?.start() - - if case let .generic(gift) = component.gift { - self.giftAuctionAcquiredGiftsDisposable = (component.context.engine.payments.getGiftAuctionAcquiredGifts(giftId: gift.id) - |> deliverOnMainQueue).startStrict(next: { [weak self] acquiredGifts in - guard let self else { - return - } - self.giftAuctionAcquiredGifts = acquiredGifts - self.state?.updated(transition: .easeInOut(duration: 0.25)) - }) - } } self.component = component @@ -1532,15 +1763,6 @@ private final class GiftAuctionBidScreenComponent: Component { if themeUpdated { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor - - var locations: [NSNumber] = [] - var colors: [CGColor] = [] - let numStops = 6 - for i in 0 ..< numStops { - let step = CGFloat(i) / CGFloat(numStops - 1) - locations.append(step as NSNumber) - colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor) - } } transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) @@ -1606,10 +1828,20 @@ private final class GiftAuctionBidScreenComponent: Component { containerSize: CGSize(width: availableSize.width - sliderInset * 2.0, height: 30.0) ) + let sliderPlusSize = self.sliderPlus.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "+", font: Font.with(size: 26.0, design: .round, weight: .regular), textColor: environment.theme.list.itemSecondaryTextColor.withAlphaComponent(0.5)))) + ), + environment: {}, + containerSize: availableSize + ) + contentHeight += 148.0 let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight), size: sliderSize) let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0)) + let sliderPlusFrame = CGRect(origin: CGPoint(x: sliderBackgroundFrame.maxX - sliderPlusSize.width - 6.0, y: sliderBackgroundFrame.minY - 3.0 + UIScreenPixel), size: sliderPlusSize) let progressFraction: CGFloat = CGFloat(self.amount.sliderValue) / CGFloat(self.amount.maxSliderValue) @@ -1619,40 +1851,63 @@ private final class GiftAuctionBidScreenComponent: Component { let color = GroupCallMessagesContext.getStarAmountParamMapping(params: liveStreamParams, value: Int64(self.amount.realValue)).color ?? GroupCallMessagesContext.Message.Color(rawValue: 0x985FDC) sliderColor = StoryLiveChatMessageComponent.getMessageColor(color: color) + var giftsPerRound: Int32 = 50 + if case let .generic(gift) = self.giftAuctionState?.gift, let giftsPerRoundValue = gift.auctionGiftsPerRound { + giftsPerRound = giftsPerRoundValue + } + + var topCutoff: CGFloat? + if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _) = giftAuctionState.auctionState { + for bidLevel in bidLevels { + if bidLevel.position == giftsPerRound - 1 { + topCutoff = CGFloat(bidLevel.amount) / CGFloat(self.amount.maxRealValue) + break + } + } + } + let _ = self.sliderBackground.update( transition: transition, component: AnyComponent(SliderBackgroundComponent( theme: environment.theme, strings: environment.strings, value: progressFraction, - topCutoff: nil, + topCutoff: topCutoff, + giftsPerRound: giftsPerRound, color: sliderColor )), environment: {}, containerSize: sliderBackgroundFrame.size ) - if let sliderView = self.slider.view, let sliderBackgroundView = self.sliderBackground.view { + if let sliderView = self.slider.view, let sliderBackgroundView = self.sliderBackground.view, let sliderPlusView = self.sliderPlus.view { if sliderView.superview == nil { self.scrollContentView.addSubview(self.badgeStars) self.scrollContentView.addSubview(sliderBackgroundView) self.scrollContentView.addSubview(sliderView) + self.scrollContentView.addSubview(sliderPlusView) + + sliderPlusView.isUserInteractionEnabled = false } transition.setFrame(view: sliderView, frame: sliderFrame) transition.setFrame(view: sliderBackgroundView, frame: sliderBackgroundFrame) + transition.setFrame(view: sliderPlusView, frame: sliderPlusFrame) + var subtitle: String? - var badgeValue = self.amount.realValue + let badgeValue: String = "\(self.amount.realValue)" var subtitleOnTop = false +// if self.amount.sliderValue == self.amount.maxSliderValue { +// badgeValue = "Custom" +// } else if let myBidAmount = self.giftAuctionState?.myState.bidAmount { if self.amount.realValue > myBidAmount { - badgeValue = self.amount.realValue - subtitle = "+\(badgeValue - Int(myBidAmount))" + subtitle = "+\(self.amount.realValue - Int(myBidAmount))" subtitleOnTop = true } else if myBidAmount == self.amount.realValue { - subtitle = "your bid" + subtitle = environment.strings.Gift_AuctionBid_YourBid } } @@ -1661,7 +1916,7 @@ private final class GiftAuctionBidScreenComponent: Component { component: AnyComponent(BadgeComponent( theme: environment.theme, prefix: nil, - title: "\(badgeValue)", + title: badgeValue, subtitle: subtitle, subtitleOnTop: subtitleOnTop, color: sliderColor @@ -1674,28 +1929,36 @@ private final class GiftAuctionBidScreenComponent: Component { let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth let sliderForegroundFrame = CGRect(origin: sliderBackgroundFrame.origin, size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height)) - var badgeFrame = CGRect(origin: CGPoint(x: sliderForegroundFrame.minX + sliderForegroundFrame.width - floorToScreenPixels(sliderMinWidth * 0.5), y: sliderForegroundFrame.minY - 8.0), size: badgeSize) + var badgeFrame = CGRect() if let badgeView = self.badge.view as? BadgeComponent.View { if badgeView.superview == nil { self.scrollContentView.insertSubview(badgeView, belowSubview: self.badgeStars) } + + let apparentBadgeSize = badgeSize - let badgeSideInset = sideInset + 15.0 + let badgeOriginX = sliderBackgroundFrame.minX + sliderForegroundFrame.width - 15.0 + badgeFrame = CGRect(origin: CGPoint(x: badgeOriginX - apparentBadgeSize.width * 0.5, y: sliderForegroundFrame.minY - 9.0 - badgeSize.height), size: apparentBadgeSize) + + let badgeSideInset: CGFloat = 23.0 let badgeOverflowWidth: CGFloat - if badgeFrame.minX - badgeSize.width * 0.5 < badgeSideInset { - badgeOverflowWidth = badgeSideInset - (badgeFrame.minX - badgeSize.width * 0.5) - } else if badgeFrame.minX + badgeSize.width * 0.5 > availableSize.width - badgeSideInset { - badgeOverflowWidth = availableSize.width - badgeSideInset - (badgeFrame.minX + badgeSize.width * 0.5) + if badgeFrame.minX < badgeSideInset { + badgeOverflowWidth = badgeSideInset - badgeFrame.minX + } else if badgeFrame.minX + badgeFrame.width > availableSize.width - badgeSideInset { + badgeOverflowWidth = availableSize.width - badgeSideInset - badgeFrame.width - badgeFrame.minX } else { badgeOverflowWidth = 0.0 } badgeFrame.origin.x += badgeOverflowWidth - - badgeView.frame = badgeFrame - - badgeView.adjustTail(size: badgeSize, overflowWidth: -badgeOverflowWidth) + let badgeTailOffset = badgeOriginX - badgeFrame.minX + let badgePosition = CGPoint(x: badgeFrame.minX + badgeTailOffset, y: badgeFrame.maxY) + + badgeView.center = badgePosition + badgeView.bounds = CGRect(origin: CGPoint(), size: badgeFrame.size) + transition.setAnchorPoint(layer: badgeView.layer, anchorPoint: CGPoint(x: max(0.0, min(1.0, badgeTailOffset / badgeFrame.width)), y: 1.0)) + badgeView.adjustTail(size: apparentBadgeSize, tailOffset: badgeTailOffset, transition: transition) } let starsRect = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: sliderForegroundFrame.midY)) @@ -1710,7 +1973,7 @@ private final class GiftAuctionBidScreenComponent: Component { var dropsLeftAnimatedItems: [AnimatedTextComponent.Item] = [] if let auctionState = self.giftAuctionState?.auctionState { - if case let .ongoing(_, minBidAmount, _, _, nextDropDate, _, dropsLeft, _) = auctionState { + if case let .ongoing(_, _, _, minBidAmount, _, _, nextDropDate, _, dropsLeft, _) = auctionState { var minBidAmount = minBidAmount if let myMinBidAmmount = self.giftAuctionState?.myState.minBidAmount { minBidAmount = myMinBidAmmount @@ -1782,7 +2045,7 @@ private final class GiftAuctionBidScreenComponent: Component { let minutes = Int(dropTimeout / 60) let seconds = Int(dropTimeout % 60) - untilNextDropAnimatedItems.append(AnimatedTextComponent.Item(id: "h", content: .number(minutes, minDigits: 2))) + untilNextDropAnimatedItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) untilNextDropAnimatedItems.append(AnimatedTextComponent.Item(id: "colon", content: .text(":"))) untilNextDropAnimatedItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) @@ -1792,17 +2055,17 @@ private final class GiftAuctionBidScreenComponent: Component { perks.append(( minBidAnimatedItems, - "minimum bid" + environment.strings.Gift_AuctionBid_MinimumBid )) perks.append(( untilNextDropAnimatedItems, - "until next drop" + environment.strings.Gift_AuctionBid_UntilNext )) perks.append(( dropsLeftAnimatedItems, - "drops left" + environment.strings.Gift_AuctionBid_Left )) contentHeight += 54.0 @@ -1827,6 +2090,8 @@ private final class GiftAuctionBidScreenComponent: Component { let _ = perkView.update( transition: transition, component: AnyComponent(AuctionStatComponent( + context: component.context, + gift: i == perks.count - 1 ? component.auctionContext.gift : nil, title: perk.0, subtitle: perk.1, theme: environment.theme @@ -1845,67 +2110,6 @@ private final class GiftAuctionBidScreenComponent: Component { contentHeight += perkHeight contentHeight += 24.0 - if self.giftAuctionAcquiredGifts.count > 0, case let .generic(gift) = component.gift { - var text = "\(self.giftAuctionAcquiredGifts.count)" - if self.giftAuctionAcquiredGifts.count == 1 { - text += " item bought" - } else { - text += " items bought" - } - - var myGiftsTransition = transition - let myGiftsSize = self.myGifts.update( - transition: .immediate, - component: AnyComponent( - PlainButtonComponent(content: AnyComponent( - HStack([ - AnyComponentWithIdentity(id: "icon", component: AnyComponent( - GiftItemComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - peer: nil, - subject: .starGift(gift: gift, price: ""), - mode: .tableIcon - ) - )), - AnyComponentWithIdentity(id: "text", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: text, font: Font.regular(15.0), textColor: environment.theme.actionSheet.controlAccentColor))) - )), - AnyComponentWithIdentity(id: "chevron", component: AnyComponent( - BundleIconComponent(name: "Settings/TextArrowRight", tintColor: environment.theme.actionSheet.controlAccentColor) - )) - ], spacing: 6.0) - ), action: { [weak self] in - guard let self, let component = self.component else { - return - } - let giftController = GiftAuctionAcquiredScreen(context: component.context, gift: component.gift, acquiredGifts: self.giftAuctionAcquiredGifts) - self.environment?.controller()?.push(giftController) - }, animateScale: false) - ), - environment: {}, - containerSize: availableSize - ) - let myGiftsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - myGiftsSize.width) / 2.0), y: contentHeight), size: myGiftsSize) - if let myGiftsView = self.myGifts.view { - if myGiftsView.superview == nil { - myGiftsTransition = .immediate - - self.scrollContentView.addSubview(myGiftsView) - - if !transition.animation.isImmediate { - transition.animateAlpha(view: myGiftsView, from: 0.0, to: 1.0) - } - } - myGiftsTransition.setFrame(view: myGiftsView, frame: myGiftsFrame) - } - contentHeight += myGiftsSize.height - contentHeight += 15.0 - } else { - - } - if self.backgroundHandleView.image == nil { self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) } @@ -1947,7 +2151,7 @@ private final class GiftAuctionBidScreenComponent: Component { transition.setFrame(view: closeButtonView, frame: closeButtonFrame) } - let infoButtonSize = self.infoButton.update( + let moreButtonSize = self.moreButton.update( transition: .immediate, component: AnyComponent(GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), @@ -1955,24 +2159,28 @@ private final class GiftAuctionBidScreenComponent: Component { isDark: environment.theme.overallDarkAppearance, state: .generic, component: AnyComponentWithIdentity(id: "info", component: AnyComponent( - BundleIconComponent( - name: "Navigation/Info", - tintColor: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor + LottieComponent( + content: LottieComponent.AppBundleContent( + name: "anim_morewide" + ), + color: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor, + size: CGSize(width: 34.0, height: 34.0), + playOnce: self.moreButtonPlayOnce ) )), - action: { [weak self] _ in - guard let self, let component = self.component else { + action: { [weak self] view in + guard let self else { return } - let giftController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: component.gift, completion: {}) - self.environment?.controller()?.push(giftController) + self.morePressed(view: view, gesture: nil) + self.moreButtonPlayOnce.invoke(Void()) } )), environment: {}, containerSize: CGSize(width: 40.0, height: 40.0) ) - let infoButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - infoButtonSize.width, y: 16.0), size: infoButtonSize) - if let infoButtonView = self.infoButton.view { + let infoButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - moreButtonSize.width, y: 16.0), size: moreButtonSize) + if let infoButtonView = self.moreButton.view { if infoButtonView.superview == nil { self.navigationBarContainer.addSubview(infoButtonView) } @@ -1987,11 +2195,10 @@ private final class GiftAuctionBidScreenComponent: Component { let title = self.title let actionButton = self.actionButton - let titleText = "Place a Bid" let titleSize = title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -2011,16 +2218,38 @@ private final class GiftAuctionBidScreenComponent: Component { var topBidsTitleComponent: AnyComponent? var topBidsComponents: [(EnginePeer.Id, AnyComponent)] = [] - if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, bidLevels, topBidders, _, _, _, _) = giftAuctionState.auctionState { - if let myBid = giftAuctionState.myState.bidAmount, let myBidDate = giftAuctionState.myState.bidDate, let peer = self.peersMap[component.context.account.peerId] { + if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, topBidders, _, _, _, _) = giftAuctionState.auctionState { + if var myBidAmount = giftAuctionState.myState.bidAmount, let myBidDate = giftAuctionState.myState.bidDate, let peer = self.peersMap[component.context.account.peerId] { var place: Int32 = 1 + var isBiddingUp = false + if self.amount.realValue > myBidAmount { + myBidAmount = Int64(self.amount.realValue) + isBiddingUp = true + } for level in bidLevels { - if myBid < level.amount || (myBid == level.amount && myBidDate > level.date) { + if myBidAmount < level.amount || (myBidAmount == level.amount && myBidDate > level.date) { place = level.position + 1 } } - myBidTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "YOUR BID", font: Font.medium(13.0), textColor: environment.theme.list.itemSecondaryTextColor)))) - myBidComponent = AnyComponent(PeerComponent(context: component.context, theme: environment.theme, groupingSeparator: environment.dateTimeFormat.groupingSeparator, peer: peer, place: place, amount: myBid, isLast: true)) + + var bidTitle: String + var bidTitleColor: UIColor + var bidStatus: PeerComponent.Status? + if isBiddingUp { + bidTitleColor = environment.theme.list.itemSecondaryTextColor + bidTitle = environment.strings.Gift_AuctionBid_BidPreview + } else if case let .generic(gift) = giftAuctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound { + bidTitle = environment.strings.Gift_AuctionBid_Outbid + bidTitleColor = environment.theme.list.itemDestructiveColor + bidStatus = .outbid + } else { + bidTitle = environment.strings.Gift_AuctionBid_Winning + bidTitleColor = environment.theme.list.itemDisclosureActions.constructive.fillColor + bidStatus = .winning + } + + myBidTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: bidTitle.uppercased(), font: Font.medium(13.0), textColor: bidTitleColor)))) + myBidComponent = AnyComponent(PeerComponent(context: component.context, theme: environment.theme, groupingSeparator: environment.dateTimeFormat.groupingSeparator, peer: peer, place: place, amount: myBidAmount, status: bidStatus, isLast: true)) } var i: Int32 = 1 @@ -2039,7 +2268,7 @@ private final class GiftAuctionBidScreenComponent: Component { } if !topBidsComponents.isEmpty { - topBidsTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "TOP 3 WINNERS", font: Font.medium(13.0), textColor: environment.theme.list.itemSecondaryTextColor)))) + topBidsTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_TopWinners.uppercased(), font: Font.medium(13.0), textColor: environment.theme.list.itemSecondaryTextColor)))) } } @@ -2204,14 +2433,14 @@ private final class GiftAuctionBidScreenComponent: Component { buttonId = "ok" } else { formattedAmount = presentationStringsFormattedNumber(Int32(clamping: self.amount.realValue - Int(myBidAmount)), environment.dateTimeFormat.groupingSeparator) - buttonString = "Add # \(formattedAmount) to Your Bid" + buttonString = environment.strings.Gift_AuctionBid_AddToBid(" # \(formattedAmount)").string buttonId = "add" } } else { - buttonString = "Place a # \(formattedAmount) Bid" + buttonString = environment.strings.Gift_AuctionBid_PlaceBid(" # \(formattedAmount)").string buttonId = "bid" } - let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.with(size: 17.0, weight: .semibold, traits: .monospacedNumbers), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) @@ -2356,14 +2585,6 @@ private final class GiftAuctionBidScreenComponent: Component { } public class GiftAuctionBidScreen: ViewControllerComponentContainer { - public final class TransitionOut { - public let sourceView: UIView - - init(sourceView: UIView) { - self.sourceView = sourceView - } - } - private let context: AccountContext private var didPlayAppearAnimation: Bool = false @@ -2651,21 +2872,30 @@ private final class SliderStarsView: UIView { } private final class AuctionStatComponent: Component { + let context: AccountContext + let gift: StarGift? let title: [AnimatedTextComponent.Item] let subtitle: String let theme: PresentationTheme init( + context: AccountContext, + gift: StarGift?, title: [AnimatedTextComponent.Item], subtitle: String, theme: PresentationTheme ) { + self.context = context + self.gift = gift self.title = title self.subtitle = subtitle self.theme = theme } static func ==(lhs: AuctionStatComponent, rhs: AuctionStatComponent) -> Bool { + if lhs.gift != rhs.gift { + return false + } if lhs.title != rhs.title { return false } @@ -2679,6 +2909,7 @@ private final class AuctionStatComponent: Component { } final class View: UIView { + let gift = ComponentView() let background = ComponentView() let title = ComponentView() let subtitle = ComponentView() @@ -2710,6 +2941,7 @@ private final class AuctionStatComponent: Component { transition.setFrame(view: backgroundView, frame: backgroundFrame) } + var titleTotalWidth: CGFloat = 0.0 let titleSize = self.title.update( transition: .spring(duration: 0.2), component: AnyComponent(AnimatedTextComponent( @@ -2722,7 +2954,30 @@ private final class AuctionStatComponent: Component { environment: {}, containerSize: backgroundFrame.size ) + titleTotalWidth += titleSize.width + var giftSize = CGSize() + if let gift = component.gift, case let .generic(gift) = gift { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + giftSize = self.gift.update( + transition: .immediate, + component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: presentationData.theme, + strings: presentationData.strings, + peer: nil, + subject: .starGift(gift: gift, price: ""), + mode: .tableIcon + ) + ), + environment: {}, + containerSize: availableSize + ) + titleTotalWidth += giftSize.width + titleTotalWidth += 4.0 + } + let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -2734,9 +2989,17 @@ private final class AuctionStatComponent: Component { let spacing: CGFloat = 2.0 - let titleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - titleSize.width) * 0.5), y: floor((backgroundFrame.height - titleSize.height - spacing - subtitleSize.height) * 0.5)), size: titleSize) + let giftFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - titleTotalWidth) * 0.5), y: floor((backgroundFrame.height - giftSize.height - spacing - subtitleSize.height) * 0.5)), size: giftSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width + titleTotalWidth) * 0.5) - titleSize.width, y: floor((backgroundFrame.height - titleSize.height - spacing - subtitleSize.height) * 0.5)), size: titleSize) let subtitleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + spacing), size: subtitleSize) + if let giftView = self.gift.view { + if giftView.superview == nil { + self.addSubview(giftView) + } + giftView.frame = giftFrame + } + if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) @@ -2763,3 +3026,17 @@ private final class AuctionStatComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +private final class GiftViewContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + + init(controller: ViewController, sourceView: UIView) { + self.controller = controller + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift index 44d814ba56..fa198c9135 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift @@ -17,7 +17,6 @@ import BundleIconComponent import Markdown import TextFormat import TelegramStringFormatting -import GiftAnimationComponent import GlassBarButtonComponent import ButtonComponent import LottieComponent @@ -89,17 +88,14 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { static var body: Body { let closeButton = Child(GlassBarButtonComponent.self) - let animation = Child(GiftCompositionComponent.self) + let icon = Child(BundleIconComponent.self) let title = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self) let list = Child(List.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 state = context.state let controller = environment.controller @@ -110,7 +106,7 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { let sideInset: CGFloat = 30.0 + environment.safeInsets.left let textSideInset: CGFloat = 30.0 + environment.safeInsets.left - let titleFont = Font.semibold(20.0) + let titleFont = Font.bold(24.0) let textFont = Font.regular(15.0) let textColor = theme.actionSheet.primaryTextColor @@ -118,46 +114,26 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { let linkColor = theme.actionSheet.controlAccentColor let spacing: CGFloat = 16.0 - var contentSize = CGSize(width: context.availableSize.width, height: 30.0) + var contentSize = CGSize(width: context.availableSize.width, height: 33.0) - var animationFile: TelegramMediaFile? - switch component.gift { - case let .generic(gift): - animationFile = gift.file - default: - animationFile = nil + var auctionGiftsPerRound: Int32 = 50 + if case let .generic(gift) = context.component.gift, let auctionGiftsPerRoundValue = gift.auctionGiftsPerRound { + auctionGiftsPerRound = auctionGiftsPerRoundValue } - - let headerHeight: CGFloat = 210.0 - let headerSubject: GiftCompositionComponent.Subject? - if let animationFile { - headerSubject = .generic(animationFile) - } else { - headerSubject = nil - } - - if let headerSubject { - let animation = animation.update( - component: GiftCompositionComponent( - context: component.context, - theme: theme, - subject: headerSubject, - animationOffset: nil, - animationScale: nil, - displayAnimationStars: false, - externalState: giftCompositionExternalState, - requestUpdate: { [weak state] _ in - state?.updated() - } - ), - 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)) - ) - } - contentSize.height += headerHeight - 78.0 + + let icon = icon.update( + component: BundleIconComponent( + name: "Premium/Auction/BidLarge", + tintColor: linkColor + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(icon + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + icon.size.height / 2.0)) + ) + contentSize.height += icon.size.height + contentSize.height += 8.0 let title = title.update( component: BalancedTextComponent( @@ -173,7 +149,7 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) ) contentSize.height += title.size.height - contentSize.height += spacing - 14.0 + contentSize.height += spacing - 8.0 let text = text.update( component: BalancedTextComponent( @@ -189,19 +165,17 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) ) contentSize.height += text.size.height - contentSize.height += spacing + 7.0 - + contentSize.height += spacing + 9.0 //TODO:localize - var items: [AnyComponentWithIdentity] = [] items.append( AnyComponentWithIdentity( id: "top", component: AnyComponent(ParagraphComponent( - title: "Top 50 Bidders", + title: "Top \(auctionGiftsPerRound) Bidders", titleColor: textColor, - text: "50 gifts are dropped in 10 rounds to the top 50 bidders by bid amount.", + text: "\(auctionGiftsPerRound) gifts are dropped in 10 rounds to the top \(auctionGiftsPerRound) bidders by bid amount.", textColor: secondaryTextColor, accentColor: linkColor, iconName: "Premium/Auction/Drop", @@ -215,7 +189,7 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { component: AnyComponent(ParagraphComponent( title: "Bid Carryover", titleColor: textColor, - text: "If your bid leaves the top 50, it will automatically join the next drop.", + text: "If your bid leaves the top \(auctionGiftsPerRound), it will automatically join the next drop.", textColor: secondaryTextColor, accentColor: linkColor, iconName: "Premium/Auction/NextDrop", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 31a12ba702..87bdc12d95 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -18,13 +18,16 @@ import Markdown import BalancedTextComponent import TextFormat import TelegramStringFormatting -import StarsAvatarComponent import PlainButtonComponent import TooltipUI import GiftAnimationComponent import ContextUI import GiftItemComponent import GlassBarButtonComponent +import ButtonComponent +import UndoUI +import LottieComponent +import AnimatedTextComponent private final class GiftAuctionViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -54,113 +57,66 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } final class State: ComponentState { - let lastSalePriceTag = GenericComponentViewTag() - let floorPriceTag = GenericComponentViewTag() let averagePriceTag = GenericComponentViewTag() private let context: AccountContext + private let auctionContext: GiftAuctionContext private let animateOut: ActionSlot> private let getController: () -> ViewController? private var disposable: Disposable? - var initialized = false + private(set) var auctionState: GiftAuctionContext.State? + + private var giftAuctionTimer: SwiftSignalKit.Timer? + fileprivate var giftAuctionAcquiredGifts: [GiftAuctionAcquiredGift] = [] + private var giftAuctionAcquiredGiftsDisposable: Disposable? var cachedStarImage: (UIImage, PresentationTheme)? - var cachedSmallStarImage: (UIImage, PresentationTheme)? - var cachedSubtitleStarImage: (UIImage, PresentationTheme)? - var cachedTonImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? var cachedSmallChevronImage: (UIImage, PresentationTheme)? init( context: AccountContext, + auctionContext: GiftAuctionContext, animateOut: ActionSlot>, getController: @escaping () -> ViewController? ) { self.context = context + self.auctionContext = auctionContext self.animateOut = animateOut self.getController = getController super.init() -// if case let .starGift(gift, _) = component.subject, gift.flags.contains(.isAuction), let giftAuctionsManager = component.context.giftAuctionsManager, let giftAuction = giftAuctionsManager.auctionContext(for: .giftId(gift.id)) { -// self.giftAuction = giftAuction -// self.giftAuctionDisposable = (giftAuction.state -// |> deliverOnMainQueue).start(next: { [weak self] state in -// guard let self else { -// return -// } -// self.giftAuctionState = state -// self.state?.updated() -// }) -// -// self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in -// self?.state?.updated() -// }, queue: Queue.mainQueue()) -// self.giftAuctionTimer?.start() -// } + self.disposable = (auctionContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.auctionState = state + self.updated() + }) -// if let _ = self.giftAuction { -// //TODO:localize -// let buttonAttributedString = NSMutableAttributedString(string: "Place a Bid", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) -// buttonTitleItems.append(AnyComponentWithIdentity(id: "bid", component: AnyComponent( -// MultilineTextComponent(text: .plain(buttonAttributedString)) -// ))) -// if let giftAuctionState = self.giftAuctionState { -// switch giftAuctionState.auctionState { -// case let .ongoing(_, _, _, _, nextDropDate, _, _, _): -// let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) -// let dropTimeout = nextDropDate - currentTime -// -// let minutes = Int(dropTimeout / 60) -// let seconds = Int(dropTimeout % 60) -// -// let rawString = environment.strings.Gift_Setup_NextDropIn -// 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: AnyHashable(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 == "m" { -// buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(minutes, minDigits: 2))) -// } else if controlString == "s" { -// buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(seconds, minDigits: 2))) -// } -// -// startIndex = endRange.upperBound -// } -// } else { -// break -// } -// } -// if startIndex != rawString.endIndex { -// buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .text(String(rawString[startIndex ..< rawString.endIndex])))) -// } -// -// buttonTitleItems.append(AnyComponentWithIdentity(id: "timer", component: AnyComponent(AnimatedTextComponent( -// font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers), -// color: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), -// items: buttonAnimatedTitleItems, -// noDelay: true -// )))) -// case .finished: -// buttonIsEnabled = false -// } -// } else { -// buttonIsLoading = true -// } -// } else { + self.giftAuctionAcquiredGiftsDisposable = (context.engine.payments.getGiftAuctionAcquiredGifts(giftId: auctionContext.gift.giftId) + |> deliverOnMainQueue).startStrict(next: { [weak self] acquiredGifts in + guard let self else { + return + } + self.giftAuctionAcquiredGifts = acquiredGifts + 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() } deinit { self.disposable?.dispose() + self.giftAuctionAcquiredGiftsDisposable?.dispose() + self.giftAuctionTimer?.invalidate() } func showAttributeInfo(tag: Any, text: String) { @@ -200,6 +156,175 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } + func openAuction() { + guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController else { + return + } + + self.dismiss(animated: true) + + let bidController = self.context.sharedContext.makeGiftAuctionBidScreen(context: self.context, auctionContext: self.auctionContext) + navigationController.pushViewController(bidController) + } + + func openPeer(_ peer: EnginePeer, dismiss: Bool = true) { + guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController else { + return + } + + controller.dismissAllTooltips() + + let context = self.context + let action = { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + chatController: nil, + context: context, + chatLocation: .peer(peer), + subject: nil, + botStart: nil, + updateTextInputState: nil, + keepStack: .always, + useExisting: true, + purposefulAction: nil, + scrollToEndIfExists: false, + activateMessageSearch: nil, + animated: true + )) + } + + if dismiss { + self.dismiss(animated: true) + Queue.mainQueue().after(0.4, { + action() + }) + } else { + action() + } + } + + func share() { + guard let controller = self.getController() as? GiftAuctionViewScreen else { + return + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + var link = "" + if case let .generic(gift) = self.auctionContext.gift, let slug = gift.auctionSlug { + link = "https://t.me/auction/\(slug)" + } + + let shareController = self.context.sharedContext.makeShareController( + context: self.context, + subject: .url(link), + forceExternal: false, + shareStory: nil, + enqueued: { [weak self, weak controller] peerIds, _ in + guard let self else { + return + } + let _ = (self.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] peerList in + guard let self else { + return + } + let peers = peerList.compactMap { $0 } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self, weak controller] action in + if let self, savedMessages, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in + guard let peer else { + return + } + self?.openPeer(peer) + Queue.mainQueue().after(0.6) { + controller?.dismiss(animated: false, completion: nil) + } + }) + } + return false + }, additionalView: nil), in: .current) + }) + }, + actionCompleted: { [weak controller] in + controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + ) + controller.present(shareController, in: .window(.root)) + } + + func morePressed(view: UIView, gesture: ContextGesture?) { + guard let controller = self.getController() as? GiftAuctionViewScreen else { + return + } + + let context = self.context + let gift = self.auctionContext.gift + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var link = "" + if case let .generic(gift) = gift, let slug = gift.auctionSlug { + link = "https://t.me/auction/\(slug)" + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Auction_Context_About, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, f in + f(.default) + + let infoController = context.sharedContext.makeGiftAuctionInfoScreen(context: context, gift: gift, completion: nil) + controller?.push(infoController) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Auction_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, f in + f(.default) + + UIPasteboard.general.string = link + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Auction_Context_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in + f(.default) + + self?.share() + }))) + + let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceView: view)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + controller.presentInGlobalOverlay(contextController) + } + func dismiss(animated: Bool) { guard let controller = self.getController() as? GiftAuctionViewScreen else { return @@ -216,24 +341,26 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, animateOut: self.animateOut, getController: self.getController) + return State(context: self.context, auctionContext: self.auctionContext, animateOut: self.animateOut, getController: self.getController) } static var body: Body { let closeButton = Child(GlassBarButtonComponent.self) - let animation = Child(GiftCompositionComponent.self) + let moreButton = Child(GlassBarButtonComponent.self) + let animation = Child(GiftItemComponent.self) - let titleBackground = Child(RoundedRectangle.self) let title = Child(MultilineTextComponent.self) - - let description = Child(MultilineTextComponent.self) - + let description = Child(BalancedTextComponent.self) + let table = Child(TableComponent.self) + + let button = Child(ButtonComponent.self) + let acquiredButton = Child(PlainButtonComponent.self) // let telegramSaleButton = Child(PlainButtonComponent.self) // let fragmentSaleButton = Child(PlainButtonComponent.self) - let giftCompositionExternalState = GiftCompositionComponent.ExternalState() + let moreButtonPlayOnce = ActionSlot() return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value @@ -247,14 +374,13 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let titleString: String = "Gift Name" - var animationFile: TelegramMediaFile? + var titleString: String = "" var giftIconSubject: GiftItemComponent.Subject? var genericGift: StarGift.Gift? switch component.auctionContext.gift { case let .generic(gift): - animationFile = gift.file + titleString = gift.title ?? "" giftIconSubject = .starGift(gift: gift, price: "") genericGift = gift default: @@ -265,43 +391,33 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let _ = genericGift var originY: CGFloat = 0.0 - - let headerHeight: CGFloat = 210.0 - let headerSubject: GiftCompositionComponent.Subject? - if let animationFile { - headerSubject = .generic(animationFile) - } else { - headerSubject = nil - } - if let headerSubject { + + if let genericGift { let animation = animation.update( - component: GiftCompositionComponent( + component: GiftItemComponent( context: component.context, theme: environment.theme, - subject: headerSubject, - animationOffset: nil, - animationScale: nil, - displayAnimationStars: false, - externalState: giftCompositionExternalState, - requestUpdate: { [weak state] _ in - state?.updated() - } + 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: context.availableSize.width, height: headerHeight), + availableSize: CGSize(width: 120.0, height: 120.0), transition: context.transition ) context.add(animation - .position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 92.0)) ) } - originY += headerHeight + originY += 177.0 let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: titleString, - font: Font.with(size: 24.0, design: .round, weight: .bold), - textColor: theme.list.itemCheckColors.foregroundColor, + font: Font.bold(24.0), + textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center )), horizontalAlignment: .center, @@ -310,54 +426,197 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) - let titleBackground = titleBackground.update( - component: RoundedRectangle(color: theme.actionSheet.controlAccentColor, cornerRadius: 24.0), - environment: {}, - availableSize: CGSize(width: title.size.width + 32.0, height: 48.0), - transition: .immediate - ) - context.add(titleBackground - .position(CGPoint(x: context.availableSize.width / 2.0, y: 187.0)) - ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 187.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 174.0)) ) - let descriptionText: String = "description" + var descriptionText: String = "" + var descriptionColor = theme.list.itemSecondaryTextColor + + let tableFont = Font.regular(15.0) + let tableTextColor = theme.list.itemPrimaryTextColor + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var endTime = currentTime + + var isEnded = false + var tableItems: [TableComponent.Item] = [] + if let auctionState = state.auctionState, case let .generic(gift) = component.auctionContext.gift { + endTime = auctionState.endDate + if case .finished = auctionState.auctionState { + isEnded = true + } else if auctionState.endDate < currentTime { + isEnded = true + } + + if isEnded { + descriptionText = strings.Gift_Auction_Ended + descriptionColor = theme.list.itemDestructiveColor + + tableItems.append(.init( + id: "firstSale", + title: strings.Gift_Auction_FirstSale, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: auctionState.startDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + tableItems.append(.init( + id: "lastSale", + title: strings.Gift_Auction_LastSale, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: auctionState.endDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + if case let .finished(_, _, averagePrice) = auctionState.auctionState { + var items: [AnyComponentWithIdentity] = [] + + let valueString = "\(presentationStringsFormattedNumber(abs(Int32(clamping: averagePrice)), dateTimeFormat.groupingSeparator))⭐️" + let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor) + let range = (valueAttributedString.string as NSString).range(of: "⭐️") + if range.location != NSNotFound { + valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range) + } + + let averagePriceString = strings.Gift_Auction_Stars(Int32(clamping: averagePrice)) + items.append(AnyComponentWithIdentity(id: "value", component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(valueAttributedString), + maximumNumberOfLines: 0 + ) + ))) + items.append(AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: "?", + color: theme.list.itemAccentColor + )), + action: { [weak state] in + guard let state else { + return + } + state.showAttributeInfo(tag: state.averagePriceTag, text: strings.Gift_Auction_AveragePriceInfo(averagePriceString, titleString).string) + } + ).tagged(state.averagePriceTag)) + )) + + tableItems.append(.init( + id: "averagePrice", + title: strings.Gift_Auction_AveragePrice, + component: AnyComponent(HStack(items, spacing: 4.0)), + insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) + )) + } + tableItems.append(.init( + id: "availability", + title: strings.Gift_Auction_Availability, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_AvailabilityOf("0", presentationStringsFormattedNumber(gift.availability?.total ?? 0, dateTimeFormat.groupingSeparator)).string, font: tableFont, textColor: tableTextColor))) + ) + )) + } else { + var auctionGiftsPerRound: Int32 = 50 + if let auctionGiftsPerRoundValue = gift.auctionGiftsPerRound { + auctionGiftsPerRound = auctionGiftsPerRoundValue + } + descriptionText = strings.Gift_Auction_Description("\(auctionGiftsPerRound)", gift.title ?? "").string + + tableItems.append(.init( + id: "start", + title: strings.Gift_Auction_Started, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: auctionState.startDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + tableItems.append(.init( + id: "ends", + title: strings.Gift_Auction_Ends, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: auctionState.endDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + if case let .ongoing(_, _, _, _, _, _, _, giftsLeft, currentRound, totalRounds) = auctionState.auctionState { + tableItems.append(.init( + id: "round", + title: strings.Gift_Auction_CurrentRound, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_Round("\(currentRound)", "\(totalRounds)").string, font: tableFont, textColor: tableTextColor))) + ) + )) + tableItems.append(.init( + id: "availability", + title: strings.Gift_Auction_Availability, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_AvailabilityOf(presentationStringsFormattedNumber(giftsLeft, dateTimeFormat.groupingSeparator), presentationStringsFormattedNumber(gift.availability?.total ?? 0, dateTimeFormat.groupingSeparator)).string, font: tableFont, textColor: tableTextColor))) + ) + )) + } + } + } + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = descriptionColor + let linkColor = theme.list.itemAccentColor + 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.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) + } + 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 description = description.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: descriptionText)) + 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 + } + }, + tapAction: { attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + let controller = component.context.sharedContext.makeGiftAuctionInfoScreen( + context: component.context, + gift: component.auctionContext.gift, + completion: nil + ) + environment.controller()?.push(controller) + } + } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(description - .position(CGPoint(x: context.availableSize.width / 2.0, y: 231.0 + description.size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 198.0 + description.size.height / 2.0)) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) originY += description.size.height originY += 42.0 - let tableFont = Font.regular(15.0) - let tableTextColor = theme.list.itemPrimaryTextColor - - var tableItems: [TableComponent.Item] = [] - tableItems.append(.init( - id: "firstSale", - title: "First Sale", - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: 0, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) - ) - )) - tableItems.append(.init( - id: "lastSale", - title: "Last Sale", - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: 0, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) - ) - )) - let table = table.update( component: TableComponent( theme: environment.theme, @@ -371,7 +630,158 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) - originY += table.size.height + 23.0 + originY += table.size.height + 26.0 + + var hasAdditionalButtons = false + if state.giftAuctionAcquiredGifts.count > 0, case let .generic(gift) = component.auctionContext.gift { + originY += 5.0 + + let text = strings.Gift_Auction_ItemsBought(Int32(state.giftAuctionAcquiredGifts.count)) + let acquiredButton = acquiredButton.update( + component: PlainButtonComponent(content: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: .starGift(gift: gift, price: ""), + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: text, font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "chevron", component: AnyComponent( + BundleIconComponent(name: "Settings/TextArrowRight", tintColor: environment.theme.actionSheet.controlAccentColor) + )) + ], spacing: 6.0) + ), action: { [weak state] in + guard let state else { + return + } + let giftController = GiftAuctionAcquiredScreen(context: component.context, gift: component.auctionContext.gift, acquiredGifts: state.giftAuctionAcquiredGifts) + environment.controller()?.push(giftController) + }, animateScale: false), + availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(acquiredButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + acquiredButton.size.height / 2.0))) + originY += acquiredButton.size.height + originY += 12.0 + + hasAdditionalButtons = true + } + + if hasAdditionalButtons { + originY += 21.0 + } + + 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 buttonChild: _UpdatedChildComponent + if !isEnded { + let buttonAttributedString = NSMutableAttributedString(string: strings.Gift_Auction_Join, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + + let 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 ? 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", 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", content: .text(String(rawString[startIndex ..< rawString.endIndex])))) + } + + let items: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers), + color: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), + items: buttonAnimatedTitleItems, + noDelay: true + ))) + ] + + buttonChild = button.update( + component: ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("buy"), + component: AnyComponent(VStack(items, spacing: 1.0)) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak state] in + guard let state else { + return + } + state.openAuction() + }), + availableSize: buttonSize, + transition: .spring(duration: 0.2) + ) + } else { + buttonChild = 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: { [weak state] in + guard let state else { + return + } + state.dismiss(animated: true) + }), + availableSize: buttonSize, + transition: context.transition + ) + } + + context.add(buttonChild + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + buttonChild.size.height / 2.0)) + ) + originY += buttonChild.size.height + originY += buttonInsets.bottom // if component.valueInfo.listedCount != nil || component.valueInfo.fragmentListedCount != nil { // originY += 5.0 @@ -500,8 +910,38 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) - let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom - return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset) + let moreButton = moreButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "more", component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent( + name: "anim_morewide" + ), + color: theme.rootController.navigationBar.glassBarButtonForegroundColor, + size: CGSize(width: 34.0, height: 34.0), + playOnce: moreButtonPlayOnce + ) + )), + action: { [weak state] view in + guard let state else { + return + } + state.morePressed(view: view, gesture: nil) + moreButtonPlayOnce.invoke(Void()) + } + ), + availableSize: CGSize(width: 40.0, height: 40.0), + transition: .immediate + ) + context.add(moreButton + .position(CGPoint(x: context.availableSize.width - 16.0 - moreButton.size.width / 2.0, y: 16.0 + moreButton.size.height / 2.0)) + ) + + return CGSize(width: context.availableSize.width, height: originY) } } } @@ -680,3 +1120,17 @@ public final class GiftAuctionViewScreen: ViewControllerComponentContainer { }) } } + +private final class GiftViewContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + + init(controller: ViewController, sourceView: UIView) { + self.controller = controller + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index f8faf8ca2c..f10ececef5 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -5108,7 +5108,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { switch action.action { - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, _, _): var reference: StarGiftReference if let peerId, let giftMessageId { reference = .message(messageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: giftMessageId)) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 7295da3847..72dbc3cc34 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -2801,9 +2801,8 @@ final class ShareWithPeersScreenComponent: Component { var subtitle: String? switch component.stateContext.subject { case .peers: - //TODO:localize if component.stateContext.liveStream { - title = "Start Live As" + title = environment.strings.Story_Privacy_StartLiveAs } else { title = environment.strings.Story_Privacy_PostStoryAs } diff --git a/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift b/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift index 647bd5f3e5..3f1ce86012 100644 --- a/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift +++ b/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift @@ -108,3 +108,49 @@ public final class UniqueGiftPreviewMedia: Media { return self.isEqual(to: other) } } + + +public final class GiftAuctionPreviewMedia: Media { + public var id: MediaId? { + return nil + } + public let peerIds: [PeerId] = [] + + public let content: StarGift.Gift? + public let centerColor: UIColor + public let edgeColor: UIColor + public let endTime: Int32 + + public init(content: StarGift.Gift, centerColor: UIColor, edgeColor: UIColor, endTime: Int32) { + self.content = content + self.centerColor = centerColor + self.edgeColor = edgeColor + self.endTime = endTime + } + + public init(decoder: PostboxDecoder) { + self.content = nil + self.centerColor = .clear + self.edgeColor = .clear + self.endTime = 0 + } + + public func encode(_ encoder: PostboxEncoder) { + } + + public func isEqual(to other: Media) -> Bool { + guard let other = other as? GiftAuctionPreviewMedia else { + return false + } + + if self.content != other.content { + return false + } + + return true + } + + public func isSemanticallyEqual(to other: Media) -> Bool { + return self.isEqual(to: other) + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidLarge.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidLarge.imageset/Contents.json new file mode 100644 index 0000000000..3ef4565c3c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidLarge.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bid_120.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidLarge.imageset/bid_120.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidLarge.imageset/bid_120.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ef957cd48aa208ccc0333ef6aff0ac291bcc5fc8 GIT binary patch literal 4220 zcmai1c{r5q_qWqTvSdv?NOof|n3U}MG9+7;27|H8FlLY?gy^+o$-Zw{A`!9^vP4-* zb`dS|QuZawZ>G{)@B6*3-~Gp2&wS2(pL6bW&huQK48JltFmH&igTMkt&M62M{cNI+g5ghbmBH-RMn*bT>G?C`coEFhqUa&Scm z5}Xhg_{UKJ8|L{bJKAx<>>=0x%jCyFjOBo>XpAqmluo+w+Sp}NvfM`|dd4FIdT z5X>q_TZ|p@r$q;ecEC9T5>RQGpNJkH`6G%44vR#%g1pnK6H#!cF_tcj*JhL`DXCdo z^KIQklFHkn*|!2+av9@D5XVk3wpub61TmfqdDVKi$xgnw&d_THlvt4wEYn>3xD zqsdZ8X5r-MQ>x`6xd=OuWAX25WIjVr(suP+fWw}JC`7|JO+=Fc=!79Enc+$f0%)iW0HJ&;lrmKeyB#@%+oYS*oQzUA7Ea4de~kNB73nzD zDTQZnm=Wc$;$`KJ*%1TKBKmszCj8c?%6J4v=`qf>L~*9gK=K*5_o>tLh3$(o z7Tz)zR3`0(Gxpm&e&lBY3%+W5b5O$1-2-V+KU07hh!_Dvpi^OtInp_+1_eb2zak=LW&JLPNozhj89C+z?c^xFhnh`x6BBqj*r=OiRIQ2>H6T>?5uJG+!gRRMk zba59Qit}gULwnlq**HOTlY5g%``A8ku|!;b%7Nf|baPb0g~#(!%}HOHiSVV4O@w;I zE7{X}bPl&Yo>IP(m4%4Wd!6(Gd6ACXVp?aOl4V@i8 zJ>hYTZSTswbnn$}XzxaEQyhOcXSZ*6d7?GHwNQZZLE>IMnc`D;P(iD~Xg)m0O%I`g zFpn*9EMqH5&NVIaHe5-4SIel^1Mj(Zr|{kboX$erkiaR`;k?Qsi_g+dbQ==*$lC?% zgQHa{=A=vf=3rMXGtDt$m=5e(ozhNYr4eO;O;MY%rm>(QEKj0n6&3V>>@MXVWdn

yj457OTS`(x-u_nCot>**0={xc{g)19b}(x*#e!+BdCp-Lt#2|FrG0qnK2=^jVu2_A`+>YL{#~l%Cn# zQH@rOcqXjv-z(7Dlz1u8y6o@LxH6~G+hqx*`(+-b6Qzfhkm}iSqtQuA6|?!rj%&GZ zRLfmoxa`z~xqcZlSV|mUdbC*i&F0H>a3$tZmfL_=xO)+f&1NWBu}|s4RrScc!a4gc z&#t?An|d~dl6nCbB-7*5^)K87OPlvuTRirDtvV7qXZW56U(nE5dj_9SzfkKwSu&Mb zzgV|6={%L?o9ungCw`ZhKi0<84)2rG=-0H`?g|L<5Undr2I^NiMjN$(l44^)YI+!Az4~z+14J_2wNeWM_5)Dn-1y4O$ zpOpwkwjo7>fEcb?@K=Kv1GvvXd2dB;aH5Gw0E-J(BX=!-2uJ1ldAsfgc>h4|!0f3` z;d8?EZS(E9?VsDG!+b+o!?bxtc%{`Q!;S7TtJJ8?=C&DW7b?ij0e00Q3d}HeAd*<5QCNxfOrI>q9H~WAk60yaGfEOtGsctb6BCop>w0Ou&64K>ogB7TV1FQC{!D6K9ec$_WK4{`2lt}1doSix zB3}9$8hPE1vTo>xZfn_kqVb=7R6 zVHF9rP}Cv@W(<1w8NqIu;8JOc4TfA&~2;Xe~d%1S1Np({-Z0q_K%@*x4e!@4`w8GnR!Yt@5#evez~1-K6hGdq`1Zag}d^cd55d?^$HR<>9ktgQmp>bq`kexurkd={UiVq|g*!7$Q}`2j zJU>*AWw?DKZuicW!9w)m{Kge$^1Pf9gMx|~DOpk2{=1LKS=9X$=?fQnFHU@ocnVI? zeSLq=zUYSQnvu3rL>08M`b+v@-9zb*HTSJWZ(f>y_{z%NYRAujx`OwK?Pgp3@mCqB zjCj^LaI|OH0!K5m`vQ7vb$Pm%G^yNbr4C!%mG`mjYSM$Hk@SZd4KEv~vF!(HYq||- zx-Db-Z(cjUo$%|OHC}Y>nsD`x_;z7;Y8O1cQt$Cz?Avlw5R%+KSeWVyohrTNmzCwH zxBAV8yNVxzQ)Wf!A@pl!1uqKum#)Di(CbNJ zbk;9|8VH^?f0s~W5{3jv(34x86n7O!Q!ZLCPn63twcgu0e#oXi65KMu8N z*65DKcW7qHamAK)MH$0Bb5MDk>A;~G6W!cF(i=P-}$c0A7}nwQY%Yz@h6uhUAiY1JtAxymui&GdxUX{dPA^=4}19oFA$8uV?EhG({?$bv1{#yw$RkErY@m zCi7=opOKGe@JUxvL0Hho2dZ2aL}QyI=TaOkRv(2*K%+1Ar;=5ZUCQL&>UjAE=(0rz zrB*m^#r4V13e`?GdwuE;$UeWw|H6Z9x}^ZTaLIoNDO{IyqTq|1r@2Y6)*0FScSGiA z*Q(qH9DmhvASR+2XajF&hrTLSA{+M6D-nY68s}QRXP;r|ENUtG_>!@wPo-}>F*Y>k z>-K&e4lHHJnXnmm8Y`yM{b*)Vp6m631CvL7L_Rp0smL?0UlsRU#*9o7RzEI-JI?I; z!JP7H`g_I9OyBi6ikkcN0WRwlwFQqg%HGWAGAchjzqiG8@a&}U{F@XUJ2g^ubmx$E z*yHZa^wxt=cB`n802j3vSlltnuCux^+_p%Dp<_;i9Tbz~!;o+%@=lJTo3eWPu2zc_ zx?MAo_p_p(3TUx7l@-Aw!tp*nXonod6H9BSs(nYANt5^^rvJx068rr_fk{DOzXlEV z3ndPkA{vdsc>vcef2tj$8*T)>1V=a=2_jvj9SWg@@dmB|;$q_8*FO&g33i12_ITvW z5b1%zV{MTh0CBj9lSdSB$`~{bi6+D(PVW&2M_>sU01^L*{U++ZWB&+AZIm4$U4VEC z;_Gwd6Hh$-pF4OEkl$VXsY#7dNG~K7jz!ud3DJpp675GRX`<~h08y460Xhh@10aAz zAE{AdkqAeQQYPN*NA;8uID`wv;m_FKgfJj+03reUQAFY|APt2|LIHcaUl@e&nt!*T z|G}gQS^W!xND`v{iAl&p2nGE&CiOq|$q?q^U;AMH^(QGML-<7fxld9G_MiMDrDgt; zpQMZwq2otqaaaV(1&IX_$4yg(&^rNwbhi+fk&u8$!(f8{{`i&RuRS+GpJtKnJf*-7BS4oSY{YAOqT3PmMmfHA=!zLy)xON zET!xbEfU$cEWepb@7w!+uj_aJG1oJn``n*%?sLxbT%RL>(A1Uz%PImS0;mfB2Fd|( zc1{2w@a$P2NE74X=7PMYiL*Ds5M0ne0-lHls;B_aSQO0&ApeIo0*^xx?a_GPNo~wk zSEMABgt|aKkDj!{psc_UMTi^?{|5);4-PdK0veA+63|rVXitni+E_>Z7mqfEwg&`h zyHK|^(e^kL`qz#=8he%C2$YAy6n`OxK*&!o9t1oZ=?WmFRV88&oP#`VIIq>V7RjH-b6p@DZbt{Y*?IRza+Q z=SoVq1fCA7wP zpd(n>y@n4n{*K9y;uXAv^Wv?jj~GdAWBY!HVDe00m50NkaToEuk&Cp&DqE3uw0XpQ zYLX>WmCPf+-=$t7M0Zws=M0ZuTLYIcJ6-c7asR7ZRx)5+(^P5r5nxxmJ0LSY&<)nS zSq{h8g`7gW_xkQ&_#!q&7(?oh9KU*gU028mmLz0hXQ3jgo=7p^>`>xEGGq5KcZl=@ zT~yxfrm^s~)43KGyY5mlcAYz3qB53m?*nEYR9%uO)x4v8OrsOatEy=jx&L(lPY*+D z0KH29gWLYOCj#ris)qrhPo17H9RM=jROK0tzAGq5550RxApmlhsWLcX z98g1#4W^f4y=TkXz<6<7h#UxHVKxFv-P@1R*dN1(VgQSnwF_A8W73F4uyU~<5IImu zKeV4)wFRMU!ql&JPUF+vh#qJ@dmVd}Drb*P&k0_pcqD)E0fFX3InLDphH(UmTaZ1E zGBa*PQncD{M#&p@SQqtW5Dv(l*CX*WA;j+h&NH{Df{mn2fKmYNFpex()}m2vzLN<< zAzOcNU2t6Xaie3s<_g>fI*^{1j^~yE3P;nU$3kQ^lX5QHP3;~1eCG3!Wvn{{#AY0hh_NY?6Xy* zPWdY(K|{8yH$0v)y;o8K%d&eN_5yg(zr8I$@p}?KKSUB}U{?Wvo_!kETR5>Gw0?cd zV~bTS&{ow=l#zoJQWYYq&XyJB79poi7LnqJx+(h%VJGT-syuQ)wL#xk*H=R{mH>~w z_d%QA1d=+*oWGGi@=| z^C(Dvwwb>|%qpyxvVq!E84*=i;`yy#T$X{mmRUv&i4iO73O4a8(kqlNN&P2|_VJts zP04oIkQ^o$*vu&=V&?XZt>CCs{#PlMDNQMu6lN$0W9U@iSnDj|l;B8oG;q9FWnINI zBr$Zf(xxY2T6h`d&DWT!lADt?nJ3m;&^O(m+zaiq&PvT0&i}4^JHPF9#H&-iGkvLj zZ{OGyK302tWjB!iX#gh1*6qpNw(-WSSY*w_Aqo%0UF?!rb?@mM@0_q!E_=mE!lct= z{c&^7`|!99k@(^J4+_#TL732K8OT7_n7*xNdvUj*{hXt$LK*CsT@0Uaq`vlF_RrK? z?83F8wIW(h>G^e@>}*W@E77LpYjIqOQ}K^E9vt}k3Rs>58r4I0fR4$VH8srYX9%@$OFdywhY<8{+LpTKL^M^@`n|9D9!GAD1+ zq0O`Hw&ALwT^_{H|1=~mKJCKk+aQ=_myOj!(rc}^p_9fRM2Wfetu?~Lgu3Y(_mRTU zjJlcHr4i@ROdm2y+&gWN^a1lha?xecw;ONgiXwVvHTX6zHhMQEG?i9{dru38boSNgz#4!pF*|QBf5LCdnUMB zPl=zZYo4NHQ@%8hh53XY4bu~o7K3Sz+%&n(rCF^#k=>l_lyl$AyKLk!UV2FOG|1a6 zp||ksrMIR^-+x%63|y8p}KQxxCzovr^LY;#-Ht7qzKv)$3U0 z@`M^FW(KEhs@$TCdlZUu?(FXxjXP5}>gD5?VU=+b=NPsWEq12&4AU7OFPR~t+uOIk zS9VtR6$SMLb%%~L&zv*gMXXZ3&ee=IYOQL8t=X=ztg+4!hkdfm%SqM?pPO>Or|5l_ z*pR@jl@0oryslnur+-i1L5~@Xt9TQ<$-E}GWtD+YK}=Zmnim+=7Aw+nhF|Q>!|qP4Ty$p0$tpC;Egx4M=3pJbJ9r4-)jmuj%-?qQV6S)wkQ=0?w?_S?tGM|Gy zf7thFsC%Z>wRQM}?{Vy7^9{SN=%LQ!j)rqwyZOH8tuIOEMt2*Oxxvs?%VqD?vSvjm zT=e5AzhS75!uGoL^ZMkqjbpD>($?Fz29_kZUM(dTch_{IZSWXvQBXcJxT^EK9;Umwu-~l#uJGou2EElxl?t8@M zrVCX6NjC?}WbF3AYx~N9mvoP^O#2JG+HG2TJaakoT|+rJQzl~u0(C9XJ)8-=|1i|9 zNw+| zxBi9wwxX$J4V()Of6)zTkNzQ^HNZd`i!oK~{!{NN{&D926}3t5+u;IaGxeUhzYyKODCTmXDrAMtzx;BmZH8ZsKs|~&Ol;KqAc<`R|NObGHOIRwR zkqa(St?tA44lLOLKA1IxmVe{!URHYO`R7vuZGs#(TFxK0i>8!3a7IL8lH;aS&!2Ta z9A+cPrPC^J`P3yiqtlY7Tc;NlY0X=7?W|OV#A^RG9yiBaYBDdub--M_ChLGo1pppbg_yu59ZTy8@i6#uJv&b7iZqwZXbe8Zdb=ys>O zR}<}9w#Xv4S$jg;UgUA+r<h$kM@cjUvL3@7i~t~w_|w=kQnM9!yPP+O!Y1mx!<5_K3cwMtv_RMUrgW% zFBfuwzffFF)vS{e1&1dV#8e$%7_T%5u{ZIj3*q+u^gmjWR_dR#mjYDbcaJImMrl2! zhQ;Ct9>B}izXlzXYi`t034y%2Hwzh{Q5d8;js(06l#`YFvHpI5{D%I1+G8?Cd*Fz8 zd$b3T*44C@qYZBwI4l8;rFx{b?H-6g;;9%AEcXlh$29i?`+I=Y!=R|a0%>=ky*GQD zMB3^9+`xm1{FCa>S=1DR_Cn(kc(enW>YWxRZGSH$IMx9Nq>ZC{fIbp?6?hVj-J3#b zDWV?P3z>GepSjaO5|A#qtAF|?QQZLKfM9vR&lJ*rfiNf(0tGs-{l>u5_xr~V_HPVE zjp|<*Se|=9zbh0xF)rBCl$f+R&t8+@?b@A=4^mjc#7ct&wTJosX W^dKPdgdgdID8b|b5)xV$wEqvCZ5O=& literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidSmall.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidSmall.imageset/Contents.json new file mode 100644 index 0000000000..0f1f44484e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidSmall.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bid_20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidSmall.imageset/bid_20.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Auction/BidSmall.imageset/bid_20.pdf new file mode 100644 index 0000000000000000000000000000000000000000..24fed9f777ad76f1fa356a92545b302656490dab GIT binary patch literal 4227 zcmai2c{o&U8@CObNJWI`plo3b29uIKvNU9m7-nR|3}c4L5<-%#$i9VSPlW7b7iB4B zEh3S~)>z9oQ|W!{`>yMI{y5h;zvq7L{haG}pW_G(ElH@90{FN;Wr3l9G=Q^n0t3LQ zQvgH*H=ysCk-H^va0(EtHYL<7poU^Es*6#~orA&tP}P(*t)9uU>STy{Z? z+W6K*mss1 z7tL=*s9|oZL>&~rf@ZlnomtKr>i7z z^73@6Rr1lCf`3=y^lh)@;Af+0GCAgVdD}`7s%@Gqp>qJ}j&TF0#{{^_H*J>aVC;fU zpxyd>zGJw3daHBp+(rg&$S#!X8%9FbHRS<`(8? zxX{SE9;mV(Ecn!^5wr&Y-BjTmq^Y=m3PD5m|90=l5n9;2eRBS?_dsQVVdLORT5KS# zG|PQkmRg1j<9rDL05g*zAby__qt1An8$}NlFzev8qywqnL9lSJ?Ge~hMmx-SNTn44 zHwF!;o>BjJFRT}q%T~=6p~Bv4(<^cq6ocd`+{4=xC(Tawryoa<4jp03X`UOmA}Lrg znl|7^f`h3=b!0N#0 zOku;Ledcn9^v}xnzHmIV3YOzajeHv>TLjLV;|qP z?^>FY?=tCtb$#usix%nN?eOj>j(NozbHskZQA(~@{;1t;ZvJpRt<&}`YOQvmnvt4e zttWJSyF|O{;!ele6n!p?E^;coQ50MFt;oG_q;RJcT0SvsJUD8tVKMdCaV7J$X0gk2 z=Z%UGmoGzx^KrxTkLF6Z?7rASN^y_UU3)!my5$lM+w~`?cB@UBoDI*)nRICPXpb@= z8`$N@8u*=*O^Hc4e=-UpZ`o~Q^_cWZ^G)!i(FZ|dc1>F)KQXp?w$g1he=Mzfu4-lU z%2>L00_m7n$};H#=7ZR>^RiD5-p&O@^vbC9savk|s*9~PBgK24A@_X^UCUcbTrOX+ z{xUKfvb1+`WKN;wHOhU6_*u-4X(!_Q810n*ZU1He99_Nmn{j25!SS1ru_vn&GJ)tO zv}6Eqo39eGXn5NY;niE*Rniq0XC~pt>C9IvP$?3`Qz|@#>Zn2V^k()>9BMmp>_m0b zRC8wYr>3_d-oacUx-)ejiWd;Dzqjtn=+lU9+-I*k2c^XhNVtIyj)}Z@;{rr zF)b)Ixa4~+Vw>kh(qm{s!eT-UG#wt0;`D+bp;RH`ajWY+|9JgW)yg>%yARhj=+4y? zT`RXyQmyWG{dy?Flc4?}9a>$46e)Eko^))<*T}Yi>Hg5v;#3=l&pEwk@Qi3_ox#-n z*re3qc!#*Scwv3dbMMlXkAwFQIH+(J$XK?@O`XMGu#*^);_f6oFYMU9eJGA7e-U~y z!PmWLcG&t#dhv?I+N9U2*TUvQ)|0#y*uz6JB_&SmWnvx|-Z<1fuS#yOSjWOkVk=>o zIUL*+-U`P(3C3ON8t5L2R;nKJ^!81&N)yF7hO9&iDfKCVl)OD9hl#Fl-uPVl{*|ua zdjIvF;L)bJGv+%8a`Wef%CS04vS!Gd?HcnM%K~x4JJY;`WW6*~pS_i&J9B*FIBu%dK>wIkbBX$WP+ghiiuo?%tN@;8CE8Q+^1LUcrpWPdhb#>bD=XPi#*c2pU-3?$|DQXj)fskKE-p;WisO&OS!mMB68`P|$AomI9)^_JlU=7q7Q>!GV)b7=*mlUh z>at?2s}@!UD=q($vQza?{$s@h8_8>@-#&bK$?ejHk0Dcu&=Y%dQ}y1LshHFlu1QFw zN6{=#J%`&Yc5Qj#Z5M5P@uj6Id|rFj$0n2bg84To4^wMi)V{?xf7e>kuSwQ#82a}5 z)s=T6K3x;0b1v;8F1}$~CpX77A#aze-9JceEkp#M>3suFFn(dxWYhVwv=H&`eErU* z>U3b@gd`i3ZRM!gDRJMzrOM6F&KRw{y4CHQZ#tgm2%HZ&Ta^Xz>skM{V!j~z>~a6c z;hwoRm$nfRA7N~R`G(zR^l(>VXYCn|om?OE_NVwWV>`9*Lr@sma@C7m+@#=yi)<+O z9f9%5eOtGFR+G54arBjP%6iB4;ELGx%az2!p2{Ayjb443FYEfIt329+ti)#bl*&n?e#rJCzZq>v8hOZ1D#*rXKk1AXfKidM?0s_}`jJIUumPcy4@dC(Xi^$8OI zC;4@Im%>%Y(?Nb_o`FXPAJ_l@WI>YoD zdk!*B-ubroDqRU+qRquT>&N?Yr+%ej?qd48+LDBH$+S6Hm`joN{&e8M<6ygb?T$Od z7M(ODzB`5O;l_8&0Uc|2Y89y}{^YW>^LO>Jyik_lwUez@ zz%9|xOovoQkZH`#ac5vVC8+?1C(qiKl%u-xtmxDVrbmfQBEp>?hi)zI9g39BXt8|G z;1S*?D|{U8AP`QL=bj_&EzU^woAIUHIvtV<-YbB-Atd3hi|1O<+9KY)EP{GXC z{8b1>+-&}g**E1lBN1;U3sv7%qqEfz*t^o!wJ6`wm-n>_bWAEFN(Sy$pY8`U-tmr& zH06AauoZXZUMom79R)fAJd(rorwh9KJ!>DJ=F6^cTwV=k;cf5%Y!ce(+)RbY#}ejl zN&(GaRqIiQ{eJW00=;RW1`lHq`#0eRO+2SKX;7sN<|a}4c<5)ntXrRrzT?0xIbkqW z;jNJ5Y7=sTQ+`o~S7&s|Bc|7{>=eqQON1lhbj{LdY?U*I56+<04GJb?{84ibTgNJe zVO-rGHeov`7&(YJ%~2yCnsYkCYFfymO?5g8goTAz@38pTh*VVsVN5SiN+)FXa_XKI@M3={;o?pSO*+HolbWFJtX!r zAd1HB4yDu-QI71oOpWbl?$nV4q%-dFpSDR9Ghk@|Dg*wRLh2QehrwiFfCKAq3`%+3 zf9zoY!Q?4k{e?kgDDnS^$tXf8S^gW7{U34)l*Iof2mfz ViewController { + return GiftAuctionActiveBidsScreen(context: context) + } + public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController { let editorSubject: Signal switch subject { diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 5b792307af..f691900ac3 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -118,6 +118,7 @@ public enum ParsedInternalUrl { case premiumGiftCode(slug: String) case messageLink(slug: String) case collectible(slug: String) + case auction(slug: String) case externalUrl(url: String) } @@ -568,6 +569,8 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou return .theme(pathComponents[1]) } else if pathComponents[0] == "nft" { return .collectible(slug: pathComponents[1]) + } else if pathComponents[0] == "auction" { + return .auction(slug: pathComponents[1]) } else if pathComponents[0] == "addlist" || pathComponents[0] == "folder" || pathComponents[0] == "list" { return .chatFolder(slug: pathComponents[1]) } else if pathComponents[0] == "boost", pathComponents.count == 2 { @@ -1185,6 +1188,15 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) |> map { gift -> ResolveInternalUrlResult in return .result(.collectible(gift: gift)) }) + case let .auction(slug): + if let giftAuctionsManager = context.giftAuctionsManager { + return .single(.progress) |> then(giftAuctionsManager.auctionContext(for: .slug(slug)) + |> map { auction -> ResolveInternalUrlResult in + return .result(.auction(auction: auction)) + }) + } else { + return .single(.result(nil)) + } case let .messageLink(slug): return .single(.progress) |> then(context.engine.peers.resolveMessageLink(slug: slug)