Gift auctions

This commit is contained in:
Ilya Laktyushin 2025-11-12 04:46:02 +04:00
parent aba2704dcb
commit 412e02ef00
48 changed files with 3061 additions and 529 deletions

View file

@ -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" = "Youve already bid in this gift auction for **%@**.";
"Gift.Auction.Ongoing.TextYourself" = "Youve already bid in this gift auction for yourself.";
"Gift.Auction.Ongoing.View" = "View";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Empty>()
private let subtitle = ComponentView<Empty>()
private let button = ComponentView<Empty>()
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<Empty>] = []
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()
}
}
}

View file

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

View file

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

View file

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

View file

@ -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] = []

View file

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

View file

@ -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<GiftAuctionContext.State, NoError>] = []
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
}
}
}

View file

@ -268,6 +268,9 @@ public enum PresentationResourceKey: Int32 {
case chatMessageAttachedContentHighlightedButtonIconLinkOutgoingWithWallpaper
case chatMessageAttachedContentHighlightedButtonIconLinkOutgoingWithoutWallpaper
case chatMessageAttachedContentButtonIconBidIncoming
case chatMessageAttachedContentButtonIconBidOutgoing
case chatCommandPanelArrowImage
case sharedMediaFileDownloadStartIcon

View file

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

View file

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

View file

@ -381,6 +381,7 @@ public final class ButtonComponent: Component {
public let background: Background
public let content: AnyComponentWithIdentity<Empty>
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<Empty>,
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
}
}

View file

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

View file

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

View file

@ -14,8 +14,7 @@ extension CameraMode {
case .video:
return strings.Story_Camera_Video
case .live:
//TODO:localize
return "LIVE"
return strings.Story_Camera_Live
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1462,6 +1462,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
case .giftCollection:
break
case .auction:
break
case .sendGift:
break
}

View file

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

View file

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

View file

@ -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<Empty>()
private let label = ComponentView<Empty>()
private let ton = ComponentView<Empty>()
private let badgeText = ComponentView<Empty>()
private let badgeBackground = ComponentView<Empty>()
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<Empty>()
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()

View file

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

View file

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

View file

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

View file

@ -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<Empty>()
private let title = ComponentView<Empty>()
private var itemsViews: [Int64: ComponentView<Empty>] = [:]
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<ViewControllerComponentContainer.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<Empty>
if let current = self.itemsViews[id] {
itemView = current
} else {
itemView = ComponentView()
self.itemsViews[id] = itemView
}
let itemSize = itemView.update(
transition: transition,
component: AnyComponent(
ActiveAuctionComponent(
context: component.context,
theme: theme,
strings: environment.strings,
dateTimeFormat: environment.dateTimeFormat,
state: auctionState,
currentTime: currentTime,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
if let giftAuctionsManager = component.context.giftAuctionsManager {
let _ = (giftAuctionsManager.auctionContext(for: .giftId(id))
|> deliverOnMainQueue).start(next: { [weak self] auction in
guard let self, let component = self.component, let auction, let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
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<ViewControllerComponentContainer.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<Empty>()
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let button = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -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<Empty>.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<Empty>] = []
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",

View file

@ -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<Action<()>>
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<Action<()>>,
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<Void>()
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<Empty>] = []
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<Empty>] = [
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers),
color: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7),
items: buttonAnimatedTitleItems,
noDelay: true
)))
]
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)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1497,6 +1497,24 @@ func openResolvedUrlImpl(
} else {
present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}
case let .auction(auctionContext):
if let auctionContext {
if let _ = auctionContext.currentBidPeerId {
let controller = context.sharedContext.makeGiftAuctionBidScreen(
context: context,
auctionContext: auctionContext
)
navigationController?.pushViewController(controller)
} else {
let controller = context.sharedContext.makeGiftAuctionViewScreen(
context: context,
auctionContext: auctionContext
)
navigationController?.pushViewController(controller)
}
} else {
present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}
case let .messageLink(link):
if let link {
if let navigationController = navigationController {

View file

@ -671,6 +671,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
convertedUrl = "https://t.me/nft/\(slug)"
}
}
} else if parsedUrl.host == "stargift_auction" {
if let components = URLComponents(string: "/?" + query) {
var slug: String?
if let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "slug" {
slug = value
}
}
}
}
if let slug {
convertedUrl = "https://t.me/auction/\(slug)"
}
}
} else if parsedUrl.host == "privatepost" {
if let components = URLComponents(string: "/?" + query) {
var channelId: Int64?

View file

@ -3854,6 +3854,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return GiftAuctionViewScreen(context: context, auctionContext: auctionContext)
}
public func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController {
return GiftAuctionActiveBidsScreen(context: context)
}
public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController {
let editorSubject: Signal<MediaEditorScreenImpl.Subject?, NoError>
switch subject {

View file

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