From ea8dc07b23c6d0d64bde76ded2dc26467215cc8e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 10 Nov 2025 18:58:38 +0400 Subject: [PATCH] Update API --- .../Sources/AccountContext.swift | 3 +- .../Payments/StarGiftsAuctions.swift | 249 ++++--- .../TelegramEngine/Payments/Stars.swift | 9 +- .../CameraScreen/Sources/CameraScreen.swift | 17 +- .../Sources/GiftOptionsScreen.swift | 63 +- .../Sources/GiftSetupScreen.swift | 163 +---- ....swift => GiftAuctionAcquiredScreen.swift} | 18 +- ...creen.swift => GiftAuctionBidScreen.swift} | 26 +- .../Sources/GiftAuctionViewScreen.swift | 682 ++++++++++++++++++ .../Sources/StarsPurchaseScreen.swift | 5 +- .../Sources/SharedAccountContext.swift | 8 +- 11 files changed, 927 insertions(+), 316 deletions(-) rename submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/{GiftAuctionBoughtScreen.swift => GiftAuctionAcquiredScreen.swift} (97%) rename submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/{GiftAuctionScreen.swift => GiftAuctionBidScreen.swift} (99%) create mode 100644 submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 2d008e1681..ca4058b102 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift index b7d74554f2..598c819538 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift @@ -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 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() @@ -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 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 { + 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] = [] + 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 } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index bf88acc9bf..245c9260e3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 7c14762c32..4a8eccd8f1 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 44fd974edd..501c91ec43 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index cf570ade35..cc9683b2bd 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -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() } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBoughtScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift similarity index 97% rename from submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBoughtScreen.swift rename to submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift index 2203f920ef..f0124eb903 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBoughtScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift @@ -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, transition: ComponentTransition) -> CGSize { + func update(component: GiftAuctionAcquiredScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: 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) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift similarity index 99% rename from submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift rename to submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 1404e06044..b87ee4f7c3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -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, transition: ComponentTransition) -> CGSize { + func update(component: GiftAuctionBidScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: 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) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift new file mode 100644 index 0000000000..31a12ba702 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -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> + let getController: () -> ViewController? + + init( + context: AccountContext, + auctionContext: GiftAuctionContext, + animateOut: ActionSlot>, + 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> + 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>, + 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.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(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.View.Tag()) as? SheetComponent.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 + }) + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 1cceec9923..ff6a9918a8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -348,7 +348,10 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let backgroundComponent: AnyComponent? 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 diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 16db8a5437..b7fd4656c3 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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 {