mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Gift auctions
This commit is contained in:
parent
aba2704dcb
commit
412e02ef00
48 changed files with 3061 additions and 529 deletions
|
|
@ -27,6 +27,12 @@ swift_library(
|
|||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import BundleIconComponent
|
||||
import ButtonComponent
|
||||
import GiftItemComponent
|
||||
import AnimatedTextComponent
|
||||
|
||||
private let titleFont = Font.semibold(15.0)
|
||||
private let subtitleFont = Font.regular(14.0)
|
||||
|
||||
final class GiftAuctionAccessoryPanel: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
|
||||
private let tapAction: () -> Void
|
||||
|
||||
private let contentNode: ASDisplayNode
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
private let separatorNode: ASDisplayNode
|
||||
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat, Bool)?
|
||||
private var states: [GiftAuctionContext.State] = []
|
||||
private var giftAuctionTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, tapAction: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.tapAction = tapAction
|
||||
|
||||
self.contentNode = ASDisplayNode()
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.contentNode)
|
||||
|
||||
self.contentNode.addSubnode(self.separatorNode)
|
||||
|
||||
self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
|
||||
if let self, let (size, leftInset, rightInset, isHidden) = self.validLayout {
|
||||
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isHidden: isHidden, transition: .immediate)
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.giftAuctionTimer?.start()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.giftAuctionTimer?.invalidate()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
self.view.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, isHidden: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, leftInset, rightInset, isHidden)
|
||||
|
||||
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: isHidden ? -size.height : 0.0), size: size))
|
||||
transition.updateAlpha(node: self.contentNode, alpha: isHidden ? 0.0 : 1.0)
|
||||
|
||||
|
||||
guard self.states.count > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
var titleItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
for auctionState in self.states {
|
||||
if case let .generic(gift) = auctionState.gift {
|
||||
titleItems.append(AnyComponentWithIdentity(id: "icon-\(gift.id)", component: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: self.context,
|
||||
theme: self.theme,
|
||||
strings: self.strings,
|
||||
peer: nil,
|
||||
subject: .starGift(gift: gift, price: ""),
|
||||
mode: .tableIcon
|
||||
)
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
let titleText: String = self.strings.ChatList_Auctions_ActiveAuction(Int32(self.states.count))
|
||||
titleItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor))))))
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
HStack(titleItems, spacing: 3.0, alignment: .left)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: 16.0, y: 9.0), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.contentNode.view.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
}
|
||||
|
||||
let subtitleText: String
|
||||
var subtitleTextColor = self.theme.rootController.navigationBar.secondaryTextColor
|
||||
var isOutbid = false
|
||||
|
||||
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
|
||||
|
||||
if self.states.count == 1, let auctionState = self.states.first {
|
||||
let place = auctionState.place ?? 1
|
||||
if case let .generic(gift) = auctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
|
||||
subtitleText = self.strings.ChatList_Auctions_Status_Single_Outbid
|
||||
subtitleTextColor = self.theme.list.itemDestructiveColor
|
||||
isOutbid = true
|
||||
} else {
|
||||
let placeText: String
|
||||
let lastDigit = place % 10
|
||||
switch lastDigit {
|
||||
case 1:
|
||||
placeText = self.strings.ChatList_Auctions_Status_Single_PlaceFirst("\(place)").string
|
||||
case 2:
|
||||
placeText = self.strings.ChatList_Auctions_Status_Single_PlaceSecond("\(place)").string
|
||||
case 3:
|
||||
placeText = self.strings.ChatList_Auctions_Status_Single_PlaceThird("\(place)").string
|
||||
default:
|
||||
placeText = self.strings.ChatList_Auctions_Status_Single_PlaceNTh("\(place)").string
|
||||
}
|
||||
subtitleText = self.strings.ChatList_Auctions_Status_Single_Winning(placeText).string
|
||||
}
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
var endTime = currentTime
|
||||
if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, _, _) = auctionState.auctionState {
|
||||
endTime = nextRoundDate
|
||||
}
|
||||
|
||||
let endTimeout = max(0, endTime - currentTime)
|
||||
|
||||
let hours = Int(endTimeout / 3600)
|
||||
let minutes = Int((endTimeout % 3600) / 60)
|
||||
let seconds = Int(endTimeout % 60)
|
||||
|
||||
if hours > 0 {
|
||||
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 1)))
|
||||
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon1", content: .text(":")))
|
||||
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
|
||||
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":")))
|
||||
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
|
||||
} else {
|
||||
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
|
||||
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":")))
|
||||
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
|
||||
}
|
||||
} else {
|
||||
var outbidCount = 0
|
||||
for auctionState in self.states {
|
||||
let place = auctionState.place ?? 1
|
||||
if case let .generic(gift) = auctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
|
||||
outbidCount += 1
|
||||
}
|
||||
}
|
||||
if outbidCount > 0 {
|
||||
if outbidCount == self.states.count {
|
||||
subtitleText = self.strings.ChatList_Auctions_Status_Many_OutbidAll
|
||||
} else {
|
||||
subtitleText = self.strings.ChatList_Auctions_Status_Many_Outbid(Int32(outbidCount))
|
||||
}
|
||||
subtitleTextColor = self.theme.list.itemDestructiveColor
|
||||
isOutbid = true
|
||||
} else {
|
||||
subtitleText = self.strings.ChatList_Auctions_Status_Many_WinningAll
|
||||
}
|
||||
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "view", content: .text(self.strings.ChatList_Auctions_View)))
|
||||
}
|
||||
|
||||
let subtitleSize = self.subtitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: subtitleText, font: subtitleFont, textColor: subtitleTextColor)))),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: 29.0), size: subtitleSize)
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
self.contentNode.view.addSubview(subtitleView)
|
||||
}
|
||||
subtitleView.frame = subtitleFrame
|
||||
}
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: .spring(duration: 0.2),
|
||||
component: AnyComponent(
|
||||
ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: self.theme.list.itemCheckColors.fillColor,
|
||||
foreground: self.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: self.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
cornerRadius: 14.0,
|
||||
isShimmering: isOutbid
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "content",
|
||||
component: AnyComponent(HStack([
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Premium/Auction/BidSmall", tintColor: self.theme.list.itemCheckColors.foregroundColor))),
|
||||
AnyComponentWithIdentity(id: "timer", component: AnyComponent(
|
||||
AnimatedTextComponent(
|
||||
font: Font.with(size: 15.0, weight: .semibold, traits: .monospacedNumbers),
|
||||
color: self.theme.list.itemCheckColors.foregroundColor,
|
||||
items: buttonAnimatedTitleItems,
|
||||
noDelay: true
|
||||
)
|
||||
))
|
||||
], spacing: 3.0))
|
||||
),
|
||||
fitToContentWidth: true,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.tapAction()
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 28.0)
|
||||
)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - buttonSize.width - 16.0, y: 14.0), size: buttonSize)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.contentNode.view.addSubview(buttonView)
|
||||
}
|
||||
buttonView.frame = buttonFrame
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||
}
|
||||
|
||||
func update(states: [GiftAuctionContext.State]) {
|
||||
self.states = states
|
||||
if let (size, leftInset, rightInset, isHidden) = self.validLayout {
|
||||
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isHidden: isHidden, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(_ transition: ContainedViewLayoutTransition) {
|
||||
let contentPosition = self.contentNode.layer.position
|
||||
|
||||
transition.animatePosition(node: self.contentNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 56.0))
|
||||
|
||||
guard let (size, _, _, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
transition.animatePositionAdditive(node: self.separatorNode, offset: CGPoint(x: 0.0, y: size.height))
|
||||
}
|
||||
|
||||
func animateOut(_ transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
let contentPosition = self.contentNode.layer.position
|
||||
transition.animatePosition(node: self.contentNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 56.0), removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
|
||||
guard let (size, _, _, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
transition.updatePosition(node: self.separatorNode, position: self.separatorNode.position.offsetBy(dx: 0.0, dy: size.height))
|
||||
}
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.tapAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +89,10 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
|||
private var locationBroadcastMessages: [EngineMessage.Id: EngineMessage]?
|
||||
private var locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel?
|
||||
|
||||
private var giftAuctionAccessoryPanel: GiftAuctionAccessoryPanel?
|
||||
private var giftAuctionStates: [GiftAuctionContext.State] = []
|
||||
private var giftAuctionDisposable: Disposable?
|
||||
|
||||
private var groupCallPanelData: GroupCallPanelData?
|
||||
public private(set) var groupCallAccessoryPanel: GroupCallNavigationAccessoryPanel?
|
||||
|
||||
|
|
@ -358,6 +362,17 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
|||
}
|
||||
}
|
||||
|
||||
if let giftAuctionsManager = context.giftAuctionsManager, case .summary = locationBroadcastPanelSource {
|
||||
self.giftAuctionDisposable = (giftAuctionsManager.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] states in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.giftAuctionStates = states
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
self.presentationDataDisposable = (self.updatedPresentationData.1
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
|
|
@ -522,6 +537,63 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
|||
}
|
||||
}
|
||||
|
||||
if !self.giftAuctionStates.isEmpty {
|
||||
let panelHeight: CGFloat = 56.0
|
||||
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight))
|
||||
additionalHeight += panelHeight
|
||||
panelStartY += panelHeight
|
||||
|
||||
let giftAuctionAccessoryPanel: GiftAuctionAccessoryPanel
|
||||
if let current = self.giftAuctionAccessoryPanel {
|
||||
giftAuctionAccessoryPanel = current
|
||||
transition.updateFrame(node: giftAuctionAccessoryPanel, frame: panelFrame)
|
||||
giftAuctionAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: transition)
|
||||
} else {
|
||||
giftAuctionAccessoryPanel = GiftAuctionAccessoryPanel(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, tapAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !"".isEmpty, self.giftAuctionStates.count == 1, let gift = self.giftAuctionStates.first?.gift, case let .generic(gift) = gift {
|
||||
if let giftAuctionsManager = self.context.giftAuctionsManager {
|
||||
let _ = (giftAuctionsManager.auctionContext(for: .giftId(gift.id))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] auction in
|
||||
guard let self, let auction else {
|
||||
return
|
||||
}
|
||||
let controller = self.context.sharedContext.makeGiftAuctionBidScreen(context: self.context, auctionContext: auction)
|
||||
self.push(controller)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let controller = self.context.sharedContext.makeGiftAuctionActiveBidsScreen(context: self.context)
|
||||
self.push(controller)
|
||||
}
|
||||
})
|
||||
if let accessoryPanelContainer = self.accessoryPanelContainer {
|
||||
accessoryPanelContainer.addSubnode(giftAuctionAccessoryPanel)
|
||||
} else {
|
||||
self.navigationBar?.additionalContentNode.addSubnode(giftAuctionAccessoryPanel)
|
||||
}
|
||||
self.giftAuctionAccessoryPanel = giftAuctionAccessoryPanel
|
||||
giftAuctionAccessoryPanel.frame = panelFrame
|
||||
|
||||
giftAuctionAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: .immediate)
|
||||
if transition.isAnimated {
|
||||
giftAuctionAccessoryPanel.animateIn(transition)
|
||||
}
|
||||
}
|
||||
giftAuctionAccessoryPanel.update(states: self.giftAuctionStates)
|
||||
} else if let giftAuctionAccessoryPanel = self.giftAuctionAccessoryPanel {
|
||||
self.giftAuctionAccessoryPanel = nil
|
||||
if transition.isAnimated {
|
||||
giftAuctionAccessoryPanel.animateOut(transition, completion: { [weak giftAuctionAccessoryPanel] in
|
||||
giftAuctionAccessoryPanel?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
giftAuctionAccessoryPanel.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let locationBroadcastPeers = self.locationBroadcastPeers, let locationBroadcastMode = self.locationBroadcastMode {
|
||||
let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight
|
||||
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue