Update API

This commit is contained in:
Ilya Laktyushin 2025-11-10 18:58:38 +04:00
parent d05425794c
commit ea8dc07b23
11 changed files with 927 additions and 316 deletions

View file

@ -1422,7 +1422,8 @@ public protocol SharedAccountContext: AnyObject {
func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, openChatTheme: (() -> Void)?, dismissed: (() -> Void)?) -> ViewController
func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController
func makeGiftAuctionInfoScreen(context: AccountContext, gift: StarGift, completion: (() -> Void)?) -> ViewController
func makeGiftAuctionScreen(context: AccountContext, gift: StarGift, auctionContext: GiftAuctionContext) -> ViewController
func makeGiftAuctionBidScreen(context: AccountContext, auctionContext: GiftAuctionContext) -> ViewController
func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext) -> ViewController
func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController

View file

@ -4,22 +4,39 @@ import MtProtoKit
import SwiftSignalKit
import TelegramApi
private func _internal_getStarGiftAuctionState(postbox: Postbox, network: Network, accountPeerId: EnginePeer.Id, giftId: Int64, version: Int32) -> Signal<(state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)?, NoError> {
return network.request(Api.functions.payments.getStarGiftAuctionState(auction: .inputStarGiftAuction(giftId: giftId), version: version))
public enum StarGiftAuctionReference: Equatable {
case giftId(Int64)
case slug(String)
var apiAuction: Api.InputStarGiftAuction {
switch self {
case let .giftId(giftId):
return .inputStarGiftAuction(giftId: giftId)
case let .slug(slug):
return .inputStarGiftAuctionSlug(slug: slug)
}
}
}
private func _internal_getStarGiftAuctionState(postbox: Postbox, network: Network, accountPeerId: EnginePeer.Id, reference: StarGiftAuctionReference, version: Int32) -> Signal<(gift: StarGift, state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)?, NoError> {
return network.request(Api.functions.payments.getStarGiftAuctionState(auction: reference.apiAuction, version: version))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.StarGiftAuctionState?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)?, NoError> in
|> mapToSignal { result -> Signal<(gift: StarGift, state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)?, NoError> in
guard let result else {
return .single(nil)
}
return postbox.transaction { transaction -> (state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)? in
return postbox.transaction { transaction -> (gift: StarGift, state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)? in
switch result {
case let .starGiftAuctionState(gift, state, userState, timeout, users):
case let .starGiftAuctionState(apiGift, state, userState, timeout, users):
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
let _ = gift
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return (
gift: gift,
state: GiftAuctionContext.State.AuctionState(apiAuctionState: state),
myState: GiftAuctionContext.State.MyState(apiAuctionUserState: userState),
timeout: timeout
@ -39,7 +56,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 finished
case finished(startDate: Int32, endDate: Int32, averagePrice: Int64)
}
public struct MyState: Equatable {
@ -50,13 +67,14 @@ public final class GiftAuctionContext {
public var acquiredCount: Int32
}
public var gift: StarGift
public var auctionState: AuctionState
public var myState: MyState
}
private let queue: Queue = .mainQueue()
private let account: Account
let gift: StarGift
public let gift: StarGift
private let disposable = MetaDisposable()
@ -64,8 +82,6 @@ public final class GiftAuctionContext {
private var myState: State.MyState?
private var timeout: Int32?
private var updateAuctionStateDisposable: Disposable?
private var updateMyStateDisposable: Disposable?
private var updateTimer: SwiftSignalKit.Timer?
private let stateValue = Promise<State?>()
@ -74,55 +90,21 @@ public final class GiftAuctionContext {
}
public convenience init(account: Account, gift: StarGift) {
self.init(account: account, gift: gift, initialAuctionState: nil, initialMyState: nil)
self.init(account: account, gift: gift, initialAuctionState: nil, initialMyState: nil, initialTimeout: nil)
}
init(account: Account, gift: StarGift, initialAuctionState: State.AuctionState?, initialMyState: State.MyState?) {
init(account: Account, gift: StarGift, initialAuctionState: State.AuctionState?, initialMyState: State.MyState?, initialTimeout: Int32?) {
self.account = account
self.gift = gift
self.auctionState = initialAuctionState
self.myState = initialMyState
self.timeout = initialTimeout
self.load()
self.updateAuctionStateDisposable = (self.account.stateManager.updatedStarGiftAuctionState()
|> mapToSignal { updates in
if let update = updates[gift.giftId] {
return .single(update)
}
return .complete()
}
|> deliverOnMainQueue).start(next: { [weak self] auctionState in
guard let self else {
return
}
if case let .ongoing(version, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion {
} else {
self.auctionState = auctionState
}
self.pushState()
})
self.updateMyStateDisposable = (self.account.stateManager.updatedStarGiftAuctionMyState()
|> mapToSignal { updates in
if let update = updates[gift.giftId] {
return .single(update)
}
return .complete()
}
|> deliverOnMainQueue).start(next: { [weak self] myState in
guard let self else {
return
}
self.myState = myState
self.pushState()
})
}
deinit {
self.updateAuctionStateDisposable?.dispose()
self.updateMyStateDisposable?.dispose()
self.updateTimer?.invalidate()
self.disposable.dispose()
}
@ -138,12 +120,12 @@ public final class GiftAuctionContext {
public func load() {
self.pushState()
self.disposable.set((_internal_getStarGiftAuctionState(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, giftId: self.gift.giftId, version: self.currentVersion)
self.disposable.set((_internal_getStarGiftAuctionState(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, reference: .giftId(self.gift.giftId), version: self.currentVersion)
|> deliverOn(self.queue)).start(next: { [weak self] data in
guard let self else {
return
}
guard let (auctionState, myState, timeout) = data else {
guard let (_, auctionState, myState, timeout) = data else {
return
}
@ -166,47 +148,26 @@ public final class GiftAuctionContext {
self.updateTimer?.start()
}))
}
func updateAuctionState(_ auctionState: GiftAuctionContext.State.AuctionState) {
self.auctionState = auctionState
self.pushState()
}
// public func placeBid(value: Int64) {
// return _internal_fetchBotPaymentForm(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, source: source, themeParams: nil)
// |> map(Optional.init)
// |> `catch` { error -> Signal<BotPaymentForm?, BuyStarGiftError> in
// if case let .starGiftResellTooEarly(timestamp) = error {
// return .fail(.starGiftResellTooEarly(timestamp))
// }
// return .fail(.generic)
// }
// |> mapToSignal { paymentForm in
// if let paymentForm {
// if let paymentPrice = paymentForm.invoice.prices.first?.amount, let price, paymentPrice > price.amount.value {
// let currencyAmount: CurrencyAmount
// if paymentForm.invoice.currency == "TON" {
// currencyAmount = CurrencyAmount(amount: StarsAmount(value: paymentPrice, nanos: 0), currency: .ton)
// } else {
// currencyAmount = CurrencyAmount(amount: StarsAmount(value: paymentPrice, nanos: 0), currency: .stars)
// }
// return .fail(.priceChanged(currencyAmount))
// }
// return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source)
// |> mapError { error -> BuyStarGiftError in
// if case let .serverProvided(text) = error {
// return .serverProvided(text)
// } else {
// return .generic
// }
// }
// |> ignoreValues
// } else {
// return .fail(.generic)
// }
// }
// }
func updateMyState(_ myState: GiftAuctionContext.State.MyState) {
self.myState = myState
self.pushState()
}
private func pushState() {
if let auctionState = self.auctionState, let myState = self.myState {
self.stateValue.set(.single(
State(auctionState: auctionState, myState: myState)
))
self.stateValue.set(
.single(State(
gift: self.gift,
auctionState: auctionState,
myState: myState
))
)
} else {
self.stateValue.set(.single(nil))
}
@ -231,8 +192,8 @@ extension GiftAuctionContext.State.AuctionState {
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)
case .starGiftAuctionStateFinished:
self = .finished
case let .starGiftAuctionStateFinished(startDate, endDate, averagePrice):
self = .finished(startDate: startDate, endDate: endDate, averagePrice: averagePrice)
case .starGiftAuctionStateNotModified:
return nil
}
@ -333,7 +294,8 @@ func _internal_getActiveGiftAuctions(account: Account, hash: Int64) -> Signal<[G
account: account,
gift: gift,
initialAuctionState: GiftAuctionContext.State.AuctionState(apiAuctionState: auctionState),
initialMyState: GiftAuctionContext.State.MyState(apiAuctionUserState: userState)
initialMyState: GiftAuctionContext.State.MyState(apiAuctionUserState: userState),
initialTimeout: nil
))
}
}
@ -351,15 +313,62 @@ public class GiftAuctionsManager {
private var auctionContexts: [Int64 : GiftAuctionContext] = [:]
private let disposable = MetaDisposable()
private var updateAuctionStateDisposable: Disposable?
private var updateMyStateDisposable: Disposable?
private let statePromise = Promise<[GiftAuctionContext.State]>([])
public var state: Signal<[GiftAuctionContext.State], NoError> {
return self.statePromise.get()
}
public init(account: Account) {
self.account = account
self.updateAuctionStateDisposable = (self.account.stateManager.updatedStarGiftAuctionState()
|> deliverOnMainQueue).start(next: { [weak self] updates in
guard let self else {
return
}
var reload = false
for (giftId, update) in updates {
if let auctionContext = self.auctionContexts[giftId] {
auctionContext.updateAuctionState(update)
} else {
reload = true
break
}
}
if reload {
self.reload()
}
})
self.updateMyStateDisposable = (self.account.stateManager.updatedStarGiftAuctionMyState()
|> deliverOnMainQueue).start(next: { [weak self] updates in
guard let self else {
return
}
var reload = false
for (giftId, update) in updates {
if let auctionContext = self.auctionContexts[giftId] {
auctionContext.updateMyState(update)
} else {
reload = true
break
}
}
if reload {
self.reload()
}
})
self.reload()
}
deinit {
self.disposable.dispose()
self.updateAuctionStateDisposable?.dispose()
self.updateMyStateDisposable?.dispose()
}
public func reload() {
@ -373,23 +382,65 @@ public class GiftAuctionsManager {
auctionContexts[auction.gift.giftId] = auction
}
self.auctionContexts = auctionContexts
self.updateState()
}))
}
public func auctionContextForGift(gift: StarGift) -> GiftAuctionContext? {
guard case .generic = gift else {
return nil
}
if let current = self.auctionContexts[gift.giftId] {
return current
public func auctionContext(for reference: StarGiftAuctionReference) -> Signal<GiftAuctionContext?, NoError> {
if case let .giftId(id) = reference, let current = self.auctionContexts[id] {
return .single(current)
} else {
let auctionContext = GiftAuctionContext(account: self.account, gift: gift)
self.auctionContexts[gift.giftId] = auctionContext
return auctionContext
return _internal_getStarGiftAuctionState(
postbox: self.account.postbox,
network: self.account.network,
accountPeerId: self.account.peerId,
reference: reference,
version: 0
) |> mapToSignal { [weak self] result in
if let self, let result {
let auctionContext = GiftAuctionContext(account: self.account, gift: result.gift, initialAuctionState: result.state, initialMyState: result.myState, initialTimeout: result.timeout)
self.auctionContexts[result.gift.giftId] = auctionContext
self.updateState()
return .single(auctionContext)
} else {
return .single(nil)
}
}
}
}
func storeAuctionContext(auctionContext: GiftAuctionContext) {
self.auctionContexts[auctionContext.gift.giftId] = auctionContext
self.updateState()
}
private func updateState() {
var signals: [Signal<GiftAuctionContext.State, NoError>] = []
for (_, auction) in self.auctionContexts {
signals.append(auction.state
|> mapToSignal { state in
if let state {
return .single(state)
} else {
return .complete()
}
})
}
self.statePromise.set(combineLatest(signals))
}
}
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 {
return nil
}
var place: Int32 = 1
for level in bidLevels {
if myBid < level.amount || (myBid == level.amount && myBidDate > level.date) {
place = level.position + 1
}
}
return place
}
}

View file

@ -729,9 +729,14 @@ private extension StarsContext.State.Transaction {
if (apiFlags & (1 << 26)) != 0 {
flags.insert(.isStarGiftDropOriginalDetails)
}
if (apiFlags & (1 << 27)) != 0 {
flags.insert(.isLiveStreamPaidMessage)
}
if (apiFlags & (1 << 28)) != 0 {
flags.insert(.isStarGiftAuctionBid)
}
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
let _ = subscriptionPeriod
self.init(flags: flags, id: id, count: CurrencyAmount(apiAmount: stars), date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }, floodskipNumber: floodskipNumber, starrefCommissionPermille: starrefCommissionPermille, starrefPeerId: starrefPeer?.peerId, starrefAmount: starrefAmount.flatMap(StarsAmount.init(apiAmount:)), paidMessageCount: paidMessageCount, premiumGiftMonths: premiumGiftMonths, adsProceedsFromDate: adsProceedsFromDate, adsProceedsToDate: adsProceedsToDate)
}
@ -786,6 +791,8 @@ public final class StarsContext {
public static let isPostsSearch = Flags(rawValue: 1 << 10)
public static let isStarGiftPrepaidUpgrade = Flags(rawValue: 1 << 11)
public static let isStarGiftDropOriginalDetails = Flags(rawValue: 1 << 12)
public static let isStarGiftAuctionBid = Flags(rawValue: 1 << 13)
public static let isLiveStreamPaidMessage = Flags(rawValue: 1 << 14)
}
public enum Peer: Equatable {

View file

@ -535,13 +535,18 @@ private final class CameraScreenComponent: CombinedComponent {
return
}
if mode == .live && self.storiesBlockedPeers == nil {
self.storiesBlockedPeers = BlockedPeersContext(account: self.context.account, subject: .stories)
self.adminedChannels.set(.single([]) |> then(self.context.engine.peers.channelsForStories()))
self.closeFriends.set(self.context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.CloseFriends()))
var isCollageEnabled = controller.cameraState.isCollageEnabled
if mode == .live {
if self.storiesBlockedPeers == nil {
self.storiesBlockedPeers = BlockedPeersContext(account: self.context.account, subject: .stories)
self.adminedChannels.set(.single([]) |> then(self.context.engine.peers.channelsForStories()))
self.closeFriends.set(self.context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.CloseFriends()))
}
self.displayingCollageSelection = false
isCollageEnabled = false
}
controller.updateCameraState({ $0.updatedMode(mode) }, transition: .spring(duration: 0.3))
controller.updateCameraState({ $0.updatedMode(mode).updatedIsCollageEnabled(isCollageEnabled) }, transition: .spring(duration: 0.3))
var flashOn = controller.cameraState.flashMode == .on
if case .video = mode, case .auto = controller.cameraState.flashMode {

View file

@ -311,7 +311,7 @@ final class GiftOptionsScreenComponent: Component {
}
let _ = (component.context.engine.payments.checkCanSendStarGift(giftId: gift.id)
|> deliverOnMainQueue).start(next: { [weak self, weak controller] result in
|> deliverOnMainQueue).start(next: { [weak self, weak controller] result in
guard let self, let controller else {
return
}
@ -362,38 +362,45 @@ final class GiftOptionsScreenComponent: Component {
return
}
if let availability = gift.availability, availability.remains == 0 {
if availability.resale > 0 {
let storeController = component.context.sharedContext.makeGiftStoreController(
if gift.flags.contains(.isAuction) {
// let giftController = component.context.sharedContext.makeGiftAuctionViewScreen(
// context: component,
// auctionContext: <#T##GiftAuctionContext#>
// )
} else {
if let availability = gift.availability, availability.remains == 0 {
if availability.resale > 0 {
let storeController = component.context.sharedContext.makeGiftStoreController(
context: component.context,
peerId: component.peerId,
gift: gift
)
mainController.push(storeController)
} else {
let giftController = GiftViewScreen(
context: component.context,
subject: .soldOutGift(gift)
)
mainController.push(giftController)
}
} else {
var forceUnique: Bool?
if let disallowedGifts = self.state?.disallowedGifts {
if disallowedGifts.contains(.limited) && !disallowedGifts.contains(.unique) {
forceUnique = true
} else if !disallowedGifts.contains(.limited) && disallowedGifts.contains(.unique) {
forceUnique = false
}
}
let giftController = GiftSetupScreen(
context: component.context,
peerId: component.peerId,
gift: gift
)
mainController.push(storeController)
} else {
let giftController = GiftViewScreen(
context: component.context,
subject: .soldOutGift(gift)
subject: .starGift(gift, forceUnique),
completion: component.completion
)
mainController.push(giftController)
}
} else {
var forceUnique: Bool?
if let disallowedGifts = self.state?.disallowedGifts {
if disallowedGifts.contains(.limited) && !disallowedGifts.contains(.unique) {
forceUnique = true
} else if !disallowedGifts.contains(.limited) && disallowedGifts.contains(.unique) {
forceUnique = false
}
}
let giftController = GiftSetupScreen(
context: component.context,
peerId: component.peerId,
subject: .starGift(gift, forceUnique),
completion: component.completion
)
mainController.push(giftController)
}
} else if case let .unique(gift) = gift {
self.transferGift(gift)

View file

@ -145,12 +145,7 @@ private final class GiftSetupScreenComponent: Component {
private var peerMap: [EnginePeer.Id: EnginePeer] = [:]
private var sendPaidMessageStars: StarsAmount?
private var giftAuction: GiftAuctionContext?
private var giftAuctionState: GiftAuctionContext.State?
private var giftAuctionDisposable: Disposable?
private var giftAuctionTimer: SwiftSignalKit.Timer?
private var cachedStarImage: (UIImage, PresentationTheme)?
private var updateDisposable: Disposable?
@ -232,8 +227,6 @@ private final class GiftSetupScreenComponent: Component {
self.inputMediaNodeDataDisposable?.dispose()
self.updateDisposable?.dispose()
self.optionsDisposable?.dispose()
self.giftAuctionDisposable?.dispose()
self.giftAuctionTimer?.invalidate()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -327,29 +320,7 @@ private final class GiftSetupScreenComponent: Component {
guard let component = self.component, let environment = self.environment else {
return
}
if case let .starGift(gift, _) = component.subject, gift.flags.contains(.isAuction), let navigationController = environment.controller()?.navigationController as? NavigationController {
let giftAuction = self.giftAuction
let openAuction = { [weak giftAuction, weak navigationController] in
guard let giftAuction, let navigationController else {
return
}
let controller = component.context.sharedContext.makeGiftAuctionScreen(context: component.context, gift: .generic(gift), auctionContext: giftAuction)
navigationController.pushViewController(controller)
}
//if self.openedAuction {
openAuction()
// } else {
// let controller = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: .generic(gift), completion: {
// openAuction()
// })
// environment.controller()?.push(controller)
// }
return
}
switch component.subject {
case let .premium(product):
if self.payWithStars, let starsPrice = product.starsPrice, let peer = self.peerMap[component.peerId] {
@ -908,24 +879,7 @@ private final class GiftSetupScreenComponent: Component {
if isSelfGift {
self.hideName = true
}
if case let .starGift(gift, _) = component.subject, gift.flags.contains(.isAuction), let giftAuctionsManager = component.context.giftAuctionsManager, let giftAuction = giftAuctionsManager.auctionContextForGift(gift: .generic(gift)) {
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()
}
var releasedBy: EnginePeer.Id?
if case let .starGift(gift, true) = component.subject, gift.upgradeStars != nil {
self.includeUpgrade = true
@ -1773,51 +1727,6 @@ private final class GiftSetupScreenComponent: Component {
contentHeight += remainingCountSize.height
contentHeight += 7.0
if starGift.flags.contains(.isAuction) {
let parsedString = parseMarkdownIntoAttributedString("50 gifts are dropped at varying intervals to the top 50 bidders by bid amount. [Learn more >]()", attributes: footerAttributes)
let auctionFooterText = NSMutableAttributedString(attributedString: parsedString)
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme {
self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme)
}
if let range = auctionFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 {
auctionFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: auctionFooterText.string))
}
let auctionFooterSize = self.auctionFooter.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(auctionFooterText),
maximumNumberOfLines: 0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { [weak self] _, _ in
guard let self, let component = self.component, case let .starGift(gift, _) = component.subject, let controller = self.environment?.controller() else {
return
}
let infoController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: .generic(gift), completion: nil)
controller.push(infoController)
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 10000.0)
)
let auctionFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: auctionFooterSize)
if let auctionFooterView = self.auctionFooter.view {
if auctionFooterView.superview == nil {
self.scrollContentView.addSubview(auctionFooterView)
}
transition.setFrame(view: auctionFooterView, frame: auctionFooterFrame)
}
contentHeight += auctionFooterSize.height
}
contentHeight += sectionSpacing
}
@ -1863,67 +1772,9 @@ private final class GiftSetupScreenComponent: Component {
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
}
var buttonIsLoading = false
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 {
buttonTitleItems.append(AnyComponentWithIdentity(id: buttonString, component: AnyComponent(
MultilineTextComponent(text: .plain(buttonAttributedString))
)))
}
buttonTitleItems.append(AnyComponentWithIdentity(id: buttonString, component: AnyComponent(
MultilineTextComponent(text: .plain(buttonAttributedString))
)))
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 32.0)
let buttonHeight: CGFloat = 52.0
@ -1942,7 +1793,7 @@ private final class GiftSetupScreenComponent: Component {
component: AnyComponent(VStack(buttonTitleItems, spacing: 1.0))
),
isEnabled: buttonIsEnabled,
displaysProgress: buttonIsLoading || self.inProgress,
displaysProgress: self.inProgress,
action: { [weak self] in
self?.proceed()
}

View file

@ -20,7 +20,7 @@ import GlassBarButtonComponent
import GiftItemComponent
import EdgeEffect
private final class GiftAuctionBoughtScreenComponent: Component {
private final class GiftAuctionAcquiredScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
@ -37,7 +37,7 @@ private final class GiftAuctionBoughtScreenComponent: Component {
self.acquiredGifts = acquiredGifts
}
static func ==(lhs: GiftAuctionBoughtScreenComponent, rhs: GiftAuctionBoughtScreenComponent) -> Bool {
static func ==(lhs: GiftAuctionAcquiredScreenComponent, rhs: GiftAuctionAcquiredScreenComponent) -> Bool {
return true
}
@ -84,7 +84,7 @@ private final class GiftAuctionBoughtScreenComponent: Component {
private var ignoreScrolling: Bool = false
private var component: GiftAuctionBoughtScreenComponent?
private var component: GiftAuctionAcquiredScreenComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var environment: ViewControllerComponentContainer.Environment?
@ -242,7 +242,7 @@ private final class GiftAuctionBoughtScreenComponent: 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)
}
func update(component: GiftAuctionBoughtScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
func update(component: GiftAuctionAcquiredScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
@ -325,7 +325,7 @@ private final class GiftAuctionBoughtScreenComponent: Component {
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "Round #1", font: tableBoldFont, textColor: tableTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: "Round #\(gift.round)", font: tableBoldFont, textColor: tableTextColor)))
)
)
], spacing: 1.0))
@ -616,7 +616,7 @@ private final class GiftAuctionBoughtScreenComponent: Component {
}
}
public class GiftAuctionBoughtScreen: ViewControllerComponentContainer {
public class GiftAuctionAcquiredScreen: ViewControllerComponentContainer {
public final class TransitionOut {
public let sourceView: UIView
@ -633,7 +633,7 @@ public class GiftAuctionBoughtScreen: ViewControllerComponentContainer {
public init(context: AccountContext, gift: StarGift, acquiredGifts: [GiftAuctionAcquiredGift]) {
self.context = context
super.init(context: context, component: GiftAuctionBoughtScreenComponent(
super.init(context: context, component: GiftAuctionAcquiredScreenComponent(
context: context,
gift: gift,
acquiredGifts: acquiredGifts
@ -660,7 +660,7 @@ public class GiftAuctionBoughtScreen: ViewControllerComponentContainer {
if !self.didPlayAppearAnimation {
self.didPlayAppearAnimation = true
if let componentView = self.node.hostView.componentView as? GiftAuctionBoughtScreenComponent.View {
if let componentView = self.node.hostView.componentView as? GiftAuctionAcquiredScreenComponent.View {
componentView.animateIn()
}
}
@ -670,7 +670,7 @@ public class GiftAuctionBoughtScreen: ViewControllerComponentContainer {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? GiftAuctionBoughtScreenComponent.View {
if let componentView = self.node.hostView.componentView as? GiftAuctionAcquiredScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)

View file

@ -729,7 +729,7 @@ private final class SliderBackgroundComponent: Component {
let topLineFrameTransition = transition
let topLineAlphaTransition = transition
/*if transition.userData(GiftAuctionScreenComponent.IsAdjustingAmountHint.self) != nil {
/*if transition.userData(GiftAuctionBidScreenComponent.IsAdjustingAmountHint.self) != nil {
topLineFrameTransition = .easeInOut(duration: 0.12)
topLineAlphaTransition = .easeInOut(duration: 0.12)
}*/
@ -786,7 +786,7 @@ private final class SliderBackgroundComponent: Component {
}
var animateTopTextAdditionalX: CGFloat = 0.0
if transition.userData(GiftAuctionScreenComponent.IsAdjustingAmountHint.self) != nil {
if transition.userData(GiftAuctionBidScreenComponent.IsAdjustingAmountHint.self) != nil {
if let previousState = self.topTextOverflowState, previousState != topTextOverflowState, topTextOverflowState.animates(from: previousState) {
animateTopTextAdditionalX = topForegroundTextView.center.x - topTextFrame.origin.x
}
@ -823,7 +823,7 @@ private final class SliderBackgroundComponent: Component {
}
}
private final class GiftAuctionScreenComponent: Component {
private final class GiftAuctionBidScreenComponent: Component {
final class IsAdjustingAmountHint {
}
@ -843,7 +843,7 @@ private final class GiftAuctionScreenComponent: Component {
self.auctionContext = auctionContext
}
static func ==(lhs: GiftAuctionScreenComponent, rhs: GiftAuctionScreenComponent) -> Bool {
static func ==(lhs: GiftAuctionBidScreenComponent, rhs: GiftAuctionBidScreenComponent) -> Bool {
return true
}
@ -998,7 +998,7 @@ private final class GiftAuctionScreenComponent: Component {
private var ignoreScrolling: Bool = false
private var component: GiftAuctionScreenComponent?
private var component: GiftAuctionBidScreenComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var environment: ViewControllerComponentContainer.Environment?
@ -1369,7 +1369,7 @@ private final class GiftAuctionScreenComponent: Component {
})
}
func update(component: GiftAuctionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
func update(component: GiftAuctionBidScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
@ -1880,7 +1880,7 @@ private final class GiftAuctionScreenComponent: Component {
guard let self, let component = self.component else {
return
}
let giftController = GiftAuctionBoughtScreen(context: component.context, gift: component.gift, acquiredGifts: self.giftAuctionAcquiredGifts)
let giftController = GiftAuctionAcquiredScreen(context: component.context, gift: component.gift, acquiredGifts: self.giftAuctionAcquiredGifts)
self.environment?.controller()?.push(giftController)
}, animateScale: false)
),
@ -2355,7 +2355,7 @@ private final class GiftAuctionScreenComponent: Component {
}
}
public class GiftAuctionScreen: ViewControllerComponentContainer {
public class GiftAuctionBidScreen: ViewControllerComponentContainer {
public final class TransitionOut {
public let sourceView: UIView
@ -2369,12 +2369,12 @@ public class GiftAuctionScreen: ViewControllerComponentContainer {
private var didPlayAppearAnimation: Bool = false
private var isDismissed: Bool = false
public init(context: AccountContext, gift: StarGift, auctionContext: GiftAuctionContext) {
public init(context: AccountContext, auctionContext: GiftAuctionContext) {
self.context = context
super.init(context: context, component: GiftAuctionScreenComponent(
super.init(context: context, component: GiftAuctionBidScreenComponent(
context: context,
gift: gift,
gift: auctionContext.gift,
auctionContext: auctionContext
), navigationBarAppearance: .none, theme: .default)
@ -2399,7 +2399,7 @@ public class GiftAuctionScreen: ViewControllerComponentContainer {
if !self.didPlayAppearAnimation {
self.didPlayAppearAnimation = true
if let componentView = self.node.hostView.componentView as? GiftAuctionScreenComponent.View {
if let componentView = self.node.hostView.componentView as? GiftAuctionBidScreenComponent.View {
componentView.animateIn()
}
}
@ -2409,7 +2409,7 @@ public class GiftAuctionScreen: ViewControllerComponentContainer {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? GiftAuctionScreenComponent.View {
if let componentView = self.node.hostView.componentView as? GiftAuctionBidScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)

View file

@ -0,0 +1,682 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import Markdown
import BalancedTextComponent
import TextFormat
import TelegramStringFormatting
import StarsAvatarComponent
import PlainButtonComponent
import TooltipUI
import GiftAnimationComponent
import ContextUI
import GiftItemComponent
import GlassBarButtonComponent
private final class GiftAuctionViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let auctionContext: GiftAuctionContext
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.auctionContext = auctionContext
self.animateOut = animateOut
self.getController = getController
}
static func ==(lhs: GiftAuctionViewSheetContent, rhs: GiftAuctionViewSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class State: ComponentState {
let lastSalePriceTag = GenericComponentViewTag()
let floorPriceTag = GenericComponentViewTag()
let averagePriceTag = GenericComponentViewTag()
private let context: AccountContext
private let animateOut: ActionSlot<Action<()>>
private let getController: () -> ViewController?
private var disposable: Disposable?
var initialized = false
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,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
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()
// }
// 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 {
}
deinit {
self.disposable?.dispose()
}
func showAttributeInfo(tag: Any, text: String) {
guard let controller = self.getController() as? GiftAuctionViewScreen else {
return
}
controller.dismissAllTooltips()
guard let sourceView = controller.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .markdown(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
controller.present(tooltipController, in: .current)
}
func openGiftResale(gift: StarGift.Gift) {
guard let controller = self.getController() as? GiftAuctionViewScreen else {
return
}
let storeController = self.context.sharedContext.makeGiftStoreController(
context: self.context,
peerId: self.context.account.peerId,
gift: gift
)
controller.push(storeController)
}
func openGiftFragmentResale(url: String) {
guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
func dismiss(animated: Bool) {
guard let controller = self.getController() as? GiftAuctionViewScreen else {
return
}
if animated {
controller.dismissAllTooltips()
self.animateOut.invoke(Action { [weak controller] _ in
controller?.dismiss(completion: nil)
})
} else {
controller.dismiss(animated: false)
}
}
}
func makeState() -> State {
return State(context: self.context, animateOut: self.animateOut, getController: self.getController)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let animation = Child(GiftCompositionComponent.self)
let titleBackground = Child(RoundedRectangle.self)
let title = Child(MultilineTextComponent.self)
let description = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self)
// let telegramSaleButton = Child(PlainButtonComponent.self)
// let fragmentSaleButton = Child(PlainButtonComponent.self)
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let dateTimeFormat = environment.dateTimeFormat
let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let titleString: String = "Gift Name"
var animationFile: TelegramMediaFile?
var giftIconSubject: GiftItemComponent.Subject?
var genericGift: StarGift.Gift?
switch component.auctionContext.gift {
case let .generic(gift):
animationFile = gift.file
giftIconSubject = .starGift(gift: gift, price: "")
genericGift = gift
default:
break
}
let _ = giftIconSubject
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 {
let animation = animation.update(
component: GiftCompositionComponent(
context: component.context,
theme: environment.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))
)
}
originY += headerHeight
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,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
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))
)
let descriptionText: String = "description"
let description = description.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: descriptionText))
),
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))
.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,
items: tableItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += table.size.height + 23.0
// if component.valueInfo.listedCount != nil || component.valueInfo.fragmentListedCount != nil {
// originY += 5.0
// }
//
// if let listedCount = component.valueInfo.listedCount, let giftIconSubject {
// let telegramSaleButton = telegramSaleButton.update(
// component: PlainButtonComponent(
// content: AnyComponent(
// HStack([
// AnyComponentWithIdentity(id: "count", component: AnyComponent(
// MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
// )),
// AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
// Rectangle(color: .clear, width: 8.0, height: 1.0)
// )),
// AnyComponentWithIdentity(id: "icon", component: AnyComponent(
// GiftItemComponent(
// context: component.context,
// theme: theme,
// strings: strings,
// peer: nil,
// subject: giftIconSubject,
// mode: .buttonIcon
// )
// )),
// AnyComponentWithIdentity(id: "label", component: AnyComponent(
// MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnTelegram)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
// )),
// AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
// BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
// ))
// ], spacing: 0.0)
// ),
// action: { [weak state] in
// guard let state, let genericGift else {
// return
// }
// state.openGiftResale(gift: genericGift)
// },
// animateScale: false
// ),
// environment: {},
// availableSize: context.availableSize,
// transition: .immediate
// )
// context.add(telegramSaleButton
// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + telegramSaleButton.size.height / 2.0))
// )
// originY += telegramSaleButton.size.height
// originY += 12.0
// }
//
// if let listedCount = component.valueInfo.fragmentListedCount, let fragmentListedUrl = component.valueInfo.fragmentListedUrl, let giftIconSubject {
// if component.valueInfo.listedCount != nil {
// originY += 18.0
// }
//
// let fragmentSaleButton = fragmentSaleButton.update(
// component: PlainButtonComponent(
// content: AnyComponent(
// HStack([
// AnyComponentWithIdentity(id: "count", component: AnyComponent(
// MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
// )),
// AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
// Rectangle(color: .clear, width: 8.0, height: 1.0)
// )),
// AnyComponentWithIdentity(id: "icon", component: AnyComponent(
// GiftItemComponent(
// context: component.context,
// theme: theme,
// strings: strings,
// peer: nil,
// subject: giftIconSubject,
// mode: .buttonIcon
// )
// )),
// AnyComponentWithIdentity(id: "label", component: AnyComponent(
// MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnFragment)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
// )),
// AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
// BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
// ))
// ], spacing: 0.0)
// ),
// action: { [weak state] in
// state?.openGiftFragmentResale(url: fragmentListedUrl)
// },
// animateScale: false
// ),
// environment: {},
// availableSize: context.availableSize,
// transition: .immediate
// )
// context.add(fragmentSaleButton
// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + fragmentSaleButton.size.height / 2.0))
// )
// originY += fragmentSaleButton.size.height
// originY += 12.0
// }
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { [weak state] _ in
guard let state else {
return
}
state.dismiss(animated: true)
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset)
}
}
}
final class GiftAuctionViewSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let auctionContext: GiftAuctionContext
init(
context: AccountContext,
auctionContext: GiftAuctionContext
) {
self.context = context
self.auctionContext = auctionContext
}
static func ==(lhs: GiftAuctionViewSheetComponent, rhs: GiftAuctionViewSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftAuctionViewSheetContent(
context: context.component.context,
auctionContext: context.component.auctionContext,
animateOut: animateOut,
getController: controller
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
if let controller = controller() as? GiftAuctionViewScreen {
controller.dismissAllTooltips()
}
},
willDismiss: {
}
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
if let controller = controller() as? GiftAuctionViewScreen {
controller.dismissAllTooltips()
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
}
} else {
if let controller = controller() as? GiftAuctionViewScreen {
controller.dismissAllTooltips()
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
var sideInset: CGFloat = 0.0
var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight)
if case .regular = environment.metrics.widthClass {
sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0
bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight
}
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public final class GiftAuctionViewScreen: ViewControllerComponentContainer {
public init(
context: AccountContext,
auctionContext: GiftAuctionContext
) {
super.init(
context: context,
component: GiftAuctionViewSheetComponent(
context: context,
auctionContext: auctionContext
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
public func dismissAnimated() {
self.dismissAllTooltips()
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
fileprivate func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
}

View file

@ -348,7 +348,10 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
let backgroundComponent: AnyComponent<Empty>?
if product.storeProduct.id == context.component.selectedProductId {
backgroundComponent = AnyComponent(
ItemShimmeringLoadingComponent(color: environment.theme.list.itemAccentColor)
ItemShimmeringLoadingComponent(
color: environment.theme.list.itemAccentColor,
cornerRadius: 26.0
)
)
} else {
backgroundComponent = nil

View file

@ -3846,8 +3846,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return GiftAuctionInfoScreen(context: context, gift: gift, completion: completion)
}
public func makeGiftAuctionScreen(context: AccountContext, gift: StarGift, auctionContext: GiftAuctionContext) -> ViewController {
return GiftAuctionScreen(context: context, gift: gift, auctionContext: auctionContext)
public func makeGiftAuctionBidScreen(context: AccountContext, auctionContext: GiftAuctionContext) -> ViewController {
return GiftAuctionBidScreen(context: context, auctionContext: auctionContext)
}
public func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext) -> ViewController {
return GiftAuctionViewScreen(context: context, auctionContext: auctionContext)
}
public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController {