From 5e17b321f8f743e4b46d96cb5ddba83fc10c5235 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 13 Nov 2025 22:06:24 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 32 +- .../Sources/AccountContext.swift | 4 +- .../Sources/TelegramBaseController.swift | 2 +- .../Payments/BotPaymentForm.swift | 31 +- submodules/TelegramUI/BUILD | 1 + .../CameraScreen/Sources/CameraScreen.swift | 14 +- .../ChatMessageGiftBubbleContentNode.swift | 6 +- .../Sources/GiftItemComponent.swift | 2 +- .../GiftAuctionTransferController.swift | 267 ++++++++++++++++ .../Sources/GiftOptionsScreen.swift | 55 ++-- .../Sources/GiftSetupScreen.swift | 176 ++++++++++- .../Components/Gifts/GiftViewScreen/BUILD | 1 + .../Sources/GiftAuctionActiveBidsScreen.swift | 2 +- .../Sources/GiftAuctionBidScreen.swift | 63 +++- .../GiftAuctionCustomBidController.swift | 290 ++++++------------ .../Sources/GiftAuctionViewScreen.swift | 34 +- .../Sources/StarsWithdrawalScreen.swift | 80 ++--- .../TelegramUI/Sources/OpenResolvedUrl.swift | 18 +- .../Sources/SharedAccountContext.swift | 8 +- 19 files changed, 743 insertions(+), 343 deletions(-) create mode 100644 submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 89f15877f4..d27c35f9be 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15167,7 +15167,12 @@ Error: %8$@"; "ScheduledMessages.Reminder.Delete" = "Delete Reminder"; "ScheduledMessages.Reminder.DeleteMany" = "Delete Reminders"; -"Gift.Setup.NextDropIn" = "next drop in {m}:{s}"; +"Gift.Setup.PlaceBid" = "Place a Bid"; +"Gift.Setup.AuctionInfo" = "%@ are dropped to the top %@ by bid amount. [Learn more >]()"; +"Gift.Setup.AuctionInfo.Gifts_1" = "%@ gift"; +"Gift.Setup.AuctionInfo.Gifts_any" = "%@ gifts"; +"Gift.Setup.AuctionInfo.Bidders_1" = "%@ bidder"; +"Gift.Setup.AuctionInfo.Bidders_any" = "%@ bidders"; "PrivacySettings.LoginEmailSetupInfo" = "Setup your email address for Telegram login codes."; @@ -15223,6 +15228,11 @@ Error: %8$@"; "Gift.AuctionBid.Top" = "TOP %@"; "Gift.AuctionBid.Custom" = "Custom"; +"Gift.AuctionBid.CustomBid.Title" = "Place a Custom Bid"; +"Gift.AuctionBid.CustomBid.Text" = "If you fall below the top %@, your bid will roll over to the next drop."; +"Gift.AuctionBid.CustomBid.Placeholder" = "Amount"; +"Gift.AuctionBid.CustomBid.Done" = "Place a Bid"; + "Gift.Auction.Context.About" = "About"; "Gift.Auction.Context.CopyLink" = "Copy Link"; "Gift.Auction.Context.Share" = "Share"; @@ -15269,11 +15279,6 @@ Error: %8$@"; "ChatList.Auctions.Status.Many.OutbidAll" = "You've been outbid in all of them."; "ChatList.Auctions.View" = "View"; -"Gift.Auction.Ongoing.Title" = "One Auction at a Time"; -"Gift.Auction.Ongoing.Text" = "You’ve already bid in this gift auction for **%@**."; -"Gift.Auction.Ongoing.TextYourself" = "You’ve already bid in this gift auction for yourself."; -"Gift.Auction.Ongoing.View" = "View"; - "Gift.Auction.Info.Title" = "Auction"; "Gift.Auction.Info.Description" = "Join the battle for exclusive gifts."; @@ -15368,7 +15373,14 @@ Error: %8$@"; "Stars.Transaction.LiveStreamPaidMessage_any" = "Fee for %@ Live Stream Messages"; "Stars.Transaction.LiveStreamPaidMessage.Text" = "You receive **%@%** of the price that you charge for each incoming message."; -"Notification.StarGift.Subtitle.NoConvert" = "We'll notify you once it becomes eligible for unique upgrades."; -"Notification.StarGift.Subtitle.OtherNoConvert" = "We'll notify %1$@ once it becomes eligible for unique upgrades."; -"Gift.View.NoConvertDescription" = "We'll notify you once it becomes eligible for unique upgrades."; -"Gift.View.OtherNoConvertDescription" = "We'll notify %1$@ once it becomes eligible for unique upgrades."; +"Notification.StarGift.Subtitle.NoConvert" = "Display this gift on your page and turn it into a collectible."; +"Notification.StarGift.Subtitle.OtherNoConvert" = "Display this gift on your page and turn it into a collectible."; +"Gift.View.NoConvertDescription" = "We will notify you once it becomes eligible for unique upgrades."; +"Gift.View.OtherNoConvertDescription" = "We will notify %1$@ once it becomes eligible for unique upgrades."; + +"Gift.AuctionTransfer.Title" = "Change Recipient"; +"Gift.AuctionTransfer.Text" = "The current recipient of this gift is **%@**. Change to **%@**?"; +"Gift.AuctionTransfer.TextFromYourself" = "The current recipient of this gift is you. Change to **%@**?"; +"Gift.AuctionTransfer.TextToYourself" = "The current recipient of this gift is **%@**. Change to yourself?"; +"Gift.AuctionTransfer.Change" = "Change"; + diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 6cfcbec923..32f6ba6e33 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1423,8 +1423,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, auctionContext: GiftAuctionContext, completion: (() -> Void)?) -> ViewController - func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, auctionContext: GiftAuctionContext) -> ViewController - func makeGiftAuctionViewScreen(context: AccountContext, toPeerId: EnginePeer.Id, auctionContext: GiftAuctionContext) -> ViewController + func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext) -> ViewController + func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping () -> Void) -> ViewController func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index c383986b0e..82b40ab092 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -565,7 +565,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { guard let self, let auction else { return } - let controller = self.context.sharedContext.makeGiftAuctionBidScreen(context: self.context, toPeerId: auction.currentBidPeerId ?? self.context.account.peerId, auctionContext: auction) + let controller = self.context.sharedContext.makeGiftAuctionBidScreen(context: self.context, toPeerId: auction.currentBidPeerId ?? self.context.account.peerId, text: nil, entities: nil, hideName: false, auctionContext: auction) self.push(controller) }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 7ae28adb21..6de13f7895 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -20,7 +20,7 @@ public enum BotPaymentInvoiceSource { case starGiftResale(slug: String, toPeerId: EnginePeer.Id, ton: Bool) case starGiftPrepaidUpgrade(peerId: EnginePeer.Id, hash: String) case starGiftDropOriginalDetails(reference: StarGiftReference) - case starGiftAuctionBid(update: Bool, hideName: Bool, peerId: EnginePeer.Id, giftId: Int64, bidAmount: Int64, text: String?, entities: [MessageTextEntity]?) + case starGiftAuctionBid(update: Bool, hideName: Bool, peerId: EnginePeer.Id?, giftId: Int64, bidAmount: Int64, text: String?, entities: [MessageTextEntity]?) } public struct BotPaymentInvoiceFields: OptionSet { @@ -426,22 +426,27 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv return reference.apiStarGiftReference(transaction: transaction).flatMap { .inputInvoiceStarGiftDropOriginalDetails(stargift: $0) } case let .starGiftAuctionBid(update, hideName, peerId, giftId, bidAmount, text, entities): - guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { - return nil - } var flags: Int32 = 0 - if hideName { - flags |= (1 << 0) - } + var inputPeer: Api.InputPeer? + var message: Api.TextWithEntities? if update { flags |= (1 << 2) } - flags |= (1 << 3) - - var message: Api.TextWithEntities? - if let text, !text.isEmpty { - flags |= (1 << 1) - message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? []) + if let peerId { + guard let peer = transaction.getPeer(peerId).flatMap(apiInputPeer) else { + return nil + } + flags |= (1 << 3) + inputPeer = peer + + if hideName { + flags |= (1 << 0) + } + + if let text, !text.isEmpty { + flags |= (1 << 1) + message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? []) + } } return .inputInvoiceStarGiftAuctionBid(flags: flags, peer: inputPeer, giftId: giftId, bidAmount: bidAmount, message: message) } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 601bf2830e..83a0f17408 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -464,6 +464,7 @@ swift_library( "//submodules/TelegramUI/Components/Stars/StarsIntroScreen", "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen", "//submodules/TelegramUI/Components/Gifts/GiftStoreScreen", + "//submodules/TelegramUI/Components/Gifts/GiftSetupScreen", "//submodules/TelegramUI/Components/ContentReportScreen", "//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 73f4e352fe..e25c76e8e1 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1946,13 +1946,13 @@ private final class CameraScreenComponent: CombinedComponent { view.animateIn() } })) - .disappear(ComponentTransition.Disappear({ view, transition, completion in - if let view = view as? CollageIconCarouselComponent.View, !transition.animation.isImmediate { - view.animateOut(completion: completion) - } else { - completion() - } - })) + .disappear(ComponentTransition.Disappear({ view, transition, completion in + if let view = view as? CollageIconCarouselComponent.View, !transition.animation.isImmediate { + view.animateOut(completion: completion) + } else { + completion() + } + })) ) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 44db6e68a2..7ebfa132a8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -556,7 +556,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _, _, _, _, toPeerId): var incoming = incoming + var convertStars = convertStars if case let .generic(gift) = gift { + if gift.flags.contains(.isAuction) { + convertStars = nil + } if let releasedBy = gift.releasedBy, let peer = item.message.peers[releasedBy], let addressName = peer.addressName { creatorButtonTitle = item.presentationData.strings.Notification_StarGift_ReleasedBy("**@\(addressName)**").string } @@ -656,7 +660,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { entities.append(MessageTextEntity(range: starsRange.range.lowerBound ..< starsRange.range.upperBound, type: .Bold)) } } else { - text = item.presentationData.strings.Notification_StarGift_Subtitle_OtherNoConvert(peerName).string + text = item.presentationData.strings.Notification_StarGift_Subtitle_OtherNoConvert } } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index aa7de4796a..49b96ca437 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -441,7 +441,7 @@ public final class GiftItemComponent: Component { let _ = loadingBackground.update( transition: transition, component: AnyComponent( - ItemShimmeringLoadingComponent(color: component.theme.list.itemAccentColor, cornerRadius: 10.0) + ItemShimmeringLoadingComponent(color: component.theme.list.itemAccentColor, cornerRadius: cornerRadius) ), environment: {}, containerSize: size diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift new file mode 100644 index 0000000000..fde4bd9984 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift @@ -0,0 +1,267 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode +import Markdown + +private final class GiftAuctionTransferAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let title: String + private let text: String + + private let titleNode: ASTextNode + private let textNode: ASTextNode + private let avatarNode: AvatarNode + private let arrowNode: ASImageNode + private let secondAvatarNode: AvatarNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeer: EnginePeer, toPeer: EnginePeer, title: String, text: String, actions: [TextAlertAction]) { + self.strings = strings + self.title = title + self.text = text + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 0 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + + self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.arrowNode) + self.addSubnode(self.secondAvatarNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + + self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeer) + self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let avatarSize = CGSize(width: 60.0, height: 60.0) + self.avatarNode.updateSize(size: avatarSize) + + let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + + if let arrowImage = self.arrowNode.image { + let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + transition.updateFrame(node: self.arrowNode, frame: arrowFrame) + } + + let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame) + + origin.y += avatarSize.height + 10.0 + + let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.0 + + let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 10.0 + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + let contentWidth = max(size.width, minActionsWidth) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +func giftAuctionTransferController(context: AccountContext, fromPeer: EnginePeer, toPeer: EnginePeer, commit: @escaping () -> Void) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let text: String + if fromPeer.id == context.account.peerId { + text = strings.Gift_AuctionTransfer_TextFromYourself(toPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string + } else if toPeer.id == context.account.peerId { + text = strings.Gift_AuctionTransfer_TextToYourself(fromPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string + } else { + text = strings.Gift_AuctionTransfer_Text(fromPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), toPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string + } + + var dismissImpl: ((Bool) -> Void)? + var contentNode: GiftAuctionTransferAlertContentNode? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: strings.Gift_AuctionTransfer_Change, action: { + dismissImpl?(true) + commit() + })] + + contentNode = GiftAuctionTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, fromPeer: fromPeer, toPeer: toPeer, title: strings.Gift_AuctionTransfer_Title, text: text, actions: actions) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 0c9e4e999e..07898b8328 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -390,44 +390,47 @@ final class GiftOptionsScreenComponent: Component { let giftController = component.context.sharedContext.makeGiftAuctionBidScreen( context: component.context, toPeerId: currentBidPeerId, + text: nil, + entities: nil, + hideName: false, auctionContext: auctionContext ) mainController.push(giftController) } else { - let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: currentBidPeerId)) - |> deliverOnMainQueue).start(next: { [weak self, weak mainController] peer in - guard let component = self?.component, let environment = self?.environment, let mainController else { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: currentBidPeerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak self, weak mainController] fromPeer, toPeer in + guard let component = self?.component, let mainController, let fromPeer, let toPeer else { return } - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let alertController = textAlertController( - context: component.context, - title: environment.strings.Gift_Auction_Ongoing_Title, - text: auctionContext.currentBidPeerId == component.context.account.peerId ? environment.strings.Gift_Auction_Ongoing_TextYourself : environment.strings.Gift_Auction_Ongoing_Text(peer?.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder) ?? "").string, - actions: [ - TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {}), - TextAlertAction(type: .defaultAction, title: environment.strings.Gift_Auction_Ongoing_View, action: { [weak mainController] in - guard let mainController else { - return - } - let giftController = component.context.sharedContext.makeGiftAuctionBidScreen( - context: component.context, - toPeerId: currentBidPeerId, - auctionContext: auctionContext - ) - mainController.push(giftController) - }) - ], - parseMarkdown: true - ) + + let alertController = giftAuctionTransferController(context: context, fromPeer: fromPeer, toPeer: toPeer, commit: { + let controller = GiftSetupScreen( + context: context, + peerId: component.peerId, + subject: .starGift(gift, nil), + completion: nil + ) + mainController.push(controller) + }) mainController.present(alertController, in: .window(.root)) }) } } else { let giftController = component.context.sharedContext.makeGiftAuctionViewScreen( context: component.context, - toPeerId: component.peerId, - auctionContext: auctionContext + auctionContext: auctionContext, + completion: { [weak mainController] in + let controller = GiftSetupScreen( + context: context, + peerId: component.peerId, + subject: .starGift(gift, nil), + completion: nil + ) + mainController?.push(controller) + } ) mainController.push(giftController) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index cc9683b2bd..61f83c6485 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -146,6 +146,11 @@ 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? @@ -227,6 +232,8 @@ private final class GiftSetupScreenComponent: Component { self.inputMediaNodeDataDisposable?.dispose() self.updateDisposable?.dispose() self.optionsDisposable?.dispose() + self.giftAuctionDisposable?.dispose() + self.giftAuctionTimer?.invalidate() } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -452,12 +459,11 @@ private final class GiftSetupScreenComponent: Component { } private func proceedWithStarGift() { - guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { + guard let component = self.component, let environment = self.environment, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { return } let context = component.context - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let peerId = component.peerId var textInputText = NSAttributedString() @@ -466,6 +472,22 @@ private final class GiftSetupScreenComponent: Component { } let entities = generateChatInputTextEntities(textInputText) + if case let .starGift(gift, _) = component.subject, gift.flags.contains(.isAuction), let navigationController = environment.controller()?.navigationController as? NavigationController, let auctionContext = self.giftAuction { + let controller = context.sharedContext.makeGiftAuctionBidScreen( + context: context, + toPeerId: peerId, + text: textInputText.string, + entities: entities, + hideName: self.hideName, + auctionContext: auctionContext + ) + environment.controller()?.dismiss() + navigationController.pushViewController(controller) + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + var finalPrice: Int64 var perUserLimit: Int32? var giftFile: TelegramMediaFile? @@ -879,6 +901,29 @@ 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 _ = (giftAuctionsManager.auctionContext(for: .giftId(gift.id)) + |> deliverOnMainQueue).start(next: { [weak self] auctionContext in + guard let self, let auctionContext else { + return + } + self.giftAuction = auctionContext + self.giftAuctionDisposable = (auctionContext.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 { @@ -1726,18 +1771,62 @@ private final class GiftSetupScreenComponent: Component { } contentHeight += remainingCountSize.height contentHeight += 7.0 + + if starGift.flags.contains(.isAuction), let giftsPerRound = starGift.auctionGiftsPerRound { + let parsedString = parseMarkdownIntoAttributedString(environment.strings.Gift_Setup_AuctionInfo(environment.strings.Gift_Setup_AuctionInfo_Gifts(giftsPerRound), environment.strings.Gift_Setup_AuctionInfo_Bidders(giftsPerRound)).string, 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, let controller = self.environment?.controller(), let auctionContext = self.giftAuction else { + return + } + let infoController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, auctionContext: auctionContext, 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 } - initialContentHeight = contentHeight if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme { self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme) } - var buttonIsEnabled = true let buttonString: String switch component.subject { @@ -1763,19 +1852,76 @@ private final class GiftSetupScreenComponent: Component { } var buttonTitleItems: [AnyComponentWithIdentity] = [] - - let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) - if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { - buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) - buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) - buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string)) - buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string)) + if let _ = self.giftAuction { + let buttonAttributedString = NSMutableAttributedString(string: environment.strings.Gift_Setup_PlaceBid, 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(_, _, endTime, _, _, _, _, _, _, _): + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + let endTimeout = max(0, endTime - currentTime) + + let hours = Int(endTimeout / 3600) + let minutes = Int((endTimeout % 3600) / 60) + let seconds = Int(endTimeout % 60) + + let rawString = hours > 0 ? environment.strings.Gift_Auction_TimeLeftHours : environment.strings.Gift_Auction_TimeLeftMinutes + var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] + var startIndex = rawString.startIndex + while true { + if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) { + if range.lowerBound != startIndex { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix", content: .text(String(rawString[startIndex ..< range.lowerBound])))) + } + + startIndex = range.upperBound + if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) { + let controlString = rawString[range.upperBound ..< endRange.lowerBound] + if controlString == "h" { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 2))) + } else if controlString == "m" { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + } else if controlString == "s" { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } + + startIndex = endRange.upperBound + } + } else { + break + } + } + if startIndex != rawString.endIndex { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(String(rawString[startIndex ..< rawString.endIndex])))) + } + + 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 { + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { + buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string)) + } + + 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 let actionButtonSize = self.actionButton.update( diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index cc7a5ca935..350b39f791 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -49,6 +49,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen", "//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController", "//submodules/ActivityIndicator", "//submodules/TelegramUI/Components/TabSelectorComponent", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift index c9b99b3350..b2a2a88d8d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift @@ -332,7 +332,7 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { } controller.dismiss() - let bidController = component.context.sharedContext.makeGiftAuctionBidScreen(context: component.context, toPeerId: auction.currentBidPeerId ?? component.context.account.peerId, auctionContext: auction) + let bidController = component.context.sharedContext.makeGiftAuctionBidScreen(context: component.context, toPeerId: auction.currentBidPeerId ?? component.context.account.peerId, text: nil, entities: nil, hideName: false, auctionContext: auction) navigationController.pushViewController(bidController) }) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 0d542eb9f3..f91bec0cf1 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -940,17 +940,26 @@ private final class GiftAuctionBidScreenComponent: Component { let context: AccountContext let toPeerId: EnginePeer.Id + let text: String? + let entities: [MessageTextEntity]? + let hideName: Bool let gift: StarGift let auctionContext: GiftAuctionContext init( context: AccountContext, toPeerId: EnginePeer.Id, + text: String?, + entities: [MessageTextEntity]?, + hideName: Bool, gift: StarGift, auctionContext: GiftAuctionContext ) { self.context = context self.toPeerId = toPeerId + self.text = text + self.entities = entities + self.hideName = hideName self.gift = gift self.auctionContext = auctionContext } @@ -994,7 +1003,7 @@ private final class GiftAuctionBidScreenComponent: Component { private static func makeSliderSteps(minRealValue: Int, maxRealValue: Int, isLogarithmic: Bool) -> [Int] { if isLogarithmic { - var sliderSteps: [Int] = [1, 10, 50, 100, 500, 1_000, 2_000, 5_000, 7_500, 15_000, 20_000, 30_000, 40_000, 50_000] + var sliderSteps: [Int] = [1, 10, 50, 100, 500, 1_000, 2_000, 5_000, 10_000, 20_000, 30_000, 40_000, 50_000] sliderSteps.removeAll(where: { $0 <= minRealValue }) sliderSteps.insert(minRealValue, at: 0) sliderSteps.removeAll(where: { $0 >= maxRealValue }) @@ -1423,6 +1432,10 @@ private final class GiftAuctionBidScreenComponent: Component { } var isUpdate = false + var myBidPeerId: EnginePeer.Id? + if let peerId = self.giftAuctionState?.myState.bidPeerId { + myBidPeerId = peerId + } if let myBidAmount = self.giftAuctionState?.myState.bidAmount { isUpdate = true if value == myBidAmount { @@ -1456,14 +1469,19 @@ private final class GiftAuctionBidScreenComponent: Component { self.isLoading = true self.state?.updated() + var peerId: EnginePeer.Id? + if !isUpdate || (myBidPeerId != nil && myBidPeerId != component.toPeerId) { + peerId = component.toPeerId + } + let source: BotPaymentInvoiceSource = .starGiftAuctionBid( update: isUpdate, - hideName: false, - peerId: component.toPeerId, + hideName: peerId != nil ? component.hideName : false, + peerId: peerId, giftId: gift.id, bidAmount: value, - text: nil, - entities: nil + text: peerId != nil ? component.text : nil, + entities: peerId != nil ? component.entities : nil ) let signal = BotCheckoutController.InputData.fetch(context: component.context, source: source) @@ -1489,7 +1507,7 @@ private final class GiftAuctionBidScreenComponent: Component { self.isLoading = false let newMaxValue = Int(Double(value) * 1.5) - var updatedAmount = self.amount.withMinAllowedRealValue(Int(value)) + var updatedAmount = self.amount.withMinAllowedRealValue(Int(value)).withRealValue(Int(value)) if newMaxValue > self.amount.maxRealValue { updatedAmount = updatedAmount.withMaxRealValue(newMaxValue) } @@ -1682,15 +1700,33 @@ private final class GiftAuctionBidScreenComponent: Component { } func presentCustomBidController() { - guard let component = self.component else { + guard let component = self.component, let environment = self.environment, case let .generic(gift) = component.gift else { return } + + guard let auctionState = self.giftAuctionState else { + return + } + + var minBidAmount: Int64 = 100 + if case let .ongoing(_, _, _, auctionMinBidAmount, _, _, _, _, _, _) = auctionState.auctionState { + minBidAmount = auctionMinBidAmount + if let myMinBidAmount = auctionState.myState.minBidAmount { + minBidAmount = myMinBidAmount + } + } + + + let giftsPerRounds = gift.auctionGiftsPerRound ?? 50 + let controller = giftAuctionCustomBidController( context: component.context, - title: "Place a Custom Bid", - text: "Description", - placeholder: "Bid", - value: 100, + title: environment.strings.Gift_AuctionBid_CustomBid_Title, + text: environment.strings.Gift_AuctionBid_CustomBid_Text("\(giftsPerRounds)").string, + placeholder: environment.strings.Gift_AuctionBid_CustomBid_Placeholder, + action: environment.strings.Gift_AuctionBid_CustomBid_Done, + minValue: minBidAmount, + value: minBidAmount, apply: { [weak self] value in guard let self else { return @@ -2698,12 +2734,15 @@ public class GiftAuctionBidScreen: ViewControllerComponentContainer { private var didPlayAppearAnimation: Bool = false private var isDismissed: Bool = false - public init(context: AccountContext, toPeerId: EnginePeer.Id, auctionContext: GiftAuctionContext) { + public init(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext) { self.context = context super.init(context: context, component: GiftAuctionBidScreenComponent( context: context, toPeerId: toPeerId, + text: text, + entities: entities, + hideName: hideName, gift: auctionContext.gift, auctionContext: auctionContext ), navigationBarAppearance: .none, theme: .default) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift index 5e0e181f55..21772881af 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift @@ -7,168 +7,23 @@ import TelegramCore import TelegramPresentationData import AccountContext import UrlEscaping +import ComponentFlow +import StarsWithdrawalScreen -private final class GiftAuctionCustomBidInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - private let textInputNode: EditableTextNode - private let placeholderNode: ASTextNode - - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? - - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) - - var text: String { - get { - return self.textInputNode.attributedText?.string ?? "" - } - set { - self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) - self.placeholderNode.isHidden = !newValue.isEmpty - } - } - - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - init(theme: PresentationTheme, placeholder: String, returnKeyType: UIReturnKeyType = .done) { - self.theme = theme - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = EditableTextNode() - self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] - self.textInputNode.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) - self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.keyboardType = .default - self.textInputNode.autocapitalizationType = .sentences - self.textInputNode.returnKeyType = returnKeyType - self.textInputNode.autocorrectionType = .default - self.textInputNode.tintColor = theme.actionSheet.controlAccentColor - self.textInputNode.keyboardType = .numberPad - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - super.init() - - self.textInputNode.delegate = self - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height))) - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.updateTextNodeText(animated: true) - self.textChanged?(editableTextNode.textView.text) - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty - } - - func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - } - - func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - } - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) - if updatedText.count > 1 { - self.textInputNode.layer.addShakeAnimation() - return false - } - if text == "\n" { - self.complete?() - return false - } - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - 20.0, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(33.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.placeholderNode.isHidden = false - - self.textInputNode.attributedText = nil - self.updateHeight?() - } -} - -private final class giftAuctionCustomBidAlertContentNode: AlertContentNode { +private final class GiftAuctionCustomBidAlertContentNode: AlertContentNode { + private let theme: PresentationTheme private let strings: PresentationStrings + private let dateTimeFormat: PresentationDateTimeFormat private let title: String private let text: String + private let placeholder: String + private let minValue: Int64 + fileprivate var value: Int64 private let titleNode: ASTextNode private let textNode: ASTextNode - let inputFieldNode: GiftAuctionCustomBidInputFieldNode + private let backgroundView = UIImageView() + let amountField = ComponentView() private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] @@ -182,7 +37,7 @@ private final class giftAuctionCustomBidAlertContentNode: AlertContentNode { var complete: (() -> Void)? { didSet { - self.inputFieldNode.complete = self.complete + //self.inputFieldNode.complete = self.complete } } @@ -190,18 +45,23 @@ private final class giftAuctionCustomBidAlertContentNode: AlertContentNode { return self.isUserInteractionEnabled } - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: Int64) { + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, actions: [TextAlertAction], title: String, text: String, placeholder: String, minValue: Int64, value: Int64) { + self.theme = ptheme self.strings = strings + self.dateTimeFormat = dateTimeFormat self.title = title self.text = text + self.placeholder = placeholder + self.minValue = minValue + self.value = value self.titleNode = ASTextNode() self.titleNode.maximumNumberOfLines = 2 self.textNode = ASTextNode() self.textNode.maximumNumberOfLines = 8 - - self.inputFieldNode = GiftAuctionCustomBidInputFieldNode(theme: ptheme, placeholder: placeholder) - self.inputFieldNode.text = "\(value)" + +// self.inputFieldNode = GiftAuctionCustomBidInputFieldNode(theme: ptheme, placeholder: placeholder) +// self.inputFieldNode.text = "\(value)" self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator.isLayerBacked = true @@ -225,7 +85,7 @@ private final class giftAuctionCustomBidAlertContentNode: AlertContentNode { self.addSubnode(self.titleNode) self.addSubnode(self.textNode) - self.addSubnode(self.inputFieldNode) +// self.addSubnode(self.inputFieldNode) self.addSubnode(self.actionNodesSeparator) @@ -237,14 +97,6 @@ private final class giftAuctionCustomBidAlertContentNode: AlertContentNode { self.addSubnode(separatorNode) } - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) - } - } - } - self.updateTheme(theme) } @@ -252,10 +104,6 @@ private final class giftAuctionCustomBidAlertContentNode: AlertContentNode { self.disposable.dispose() } - var value: String { - return self.inputFieldNode.text - } - override func updateTheme(_ theme: AlertControllerTheme) { self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) @@ -327,13 +175,52 @@ private final class giftAuctionCustomBidAlertContentNode: AlertContentNode { let resultWidth = contentWidth + insets.left + insets.right - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + let fieldWidth = resultWidth - 18.0 + let amountFieldSize = self.amountField.update( + transition: .immediate, + component: AnyComponent( + AmountFieldComponent( + textColor: self.theme.list.itemPrimaryTextColor, + secondaryColor: self.theme.list.itemSecondaryTextColor, + placeholderColor: self.theme.list.itemPlaceholderTextColor, + accentColor: self.theme.list.itemAccentColor, + value: self.value, + minValue: self.minValue, + forceMinValue: false, + allowZero: false, + maxValue: nil, + placeholderText: self.placeholder, + textFieldOffset: CGPoint(x: -4.0, y: -1.0), + labelText: nil, + currency: .stars, + dateTimeFormat: self.dateTimeFormat, + amountUpdated: { [weak self] value in + guard let self else { + return + } + if let value { + self.value = value + } + }, + tag: nil + ) + ), + environment: {}, + containerSize: CGSize(width: fieldWidth, height: 44.0) + ) + let amountFieldFrame = CGRect(origin: CGPoint(x: floor((resultWidth - fieldWidth) / 2.0), y: origin.y - 2.0), size: amountFieldSize) + if let amountFieldView = self.amountField.view { + if amountFieldView.superview == nil { + self.backgroundView.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + + self.view.addSubview(self.backgroundView) + self.view.addSubview(amountFieldView) + } + self.backgroundView.frame = amountFieldFrame.insetBy(dx: 7.0, dy: 9.0) + amountFieldView.frame = amountFieldFrame + } - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + amountFieldSize.height + actionsHeight + insets.top + insets.bottom + 3.0) transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) @@ -381,19 +268,36 @@ private final class giftAuctionCustomBidAlertContentNode: AlertContentNode { } if !hadValidLayout { - self.inputFieldNode.activateInput() + if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { + amountFieldView.activateInput() + amountFieldView.selectAll() + } } return resultSize } func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() + if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { + self.value = self.minValue + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + amountFieldView.resetValue() + + amountFieldView.animateError() + amountFieldView.selectAll() + } + } + + func deactivateInput() { + if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { + amountFieldView.deactivateInput() + } } } -func giftAuctionCustomBidController(context: AccountContext, title: String, text: String, placeholder: String, value: Int64, apply: @escaping (Int64) -> Void) -> AlertController { +func giftAuctionCustomBidController(context: AccountContext, title: String, text: String, placeholder: String, action: String, minValue: Int64, value: Int64, apply: @escaping (Int64) -> Void) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var dismissImpl: ((Bool) -> Void)? @@ -401,11 +305,11 @@ func giftAuctionCustomBidController(context: AccountContext, title: String, text let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: "Place a Bid", action: { + }), TextAlertAction(type: .defaultAction, title: action, action: { applyImpl?() })] - let contentNode = giftAuctionCustomBidAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value) + let contentNode = GiftAuctionCustomBidAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, actions: actions, title: title, text: text, placeholder: placeholder, minValue: minValue, value: value) contentNode.complete = { applyImpl?() } @@ -413,23 +317,25 @@ func giftAuctionCustomBidController(context: AccountContext, title: String, text guard let contentNode = contentNode else { return } - dismissImpl?(true) - - if let value = Int64(contentNode.value.trimmingCharacters(in: .whitespacesAndNewlines)) { - apply(value) + let value = contentNode.value + if value < minValue { + contentNode.animateError() + } else { + dismissImpl?(true) + apply(contentNode.value) } } let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in + let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller] presentationData in controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) }) controller.dismissed = { _ in presentationDataDisposable.dispose() } dismissImpl = { [weak controller, weak contentNode] animated in - contentNode?.inputFieldNode.deactivateInput() + contentNode?.deactivateInput() + let _ = contentNode if animated { controller?.dismissAnimated() } else { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index b99f1d99ea..ade03e43d7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -33,20 +33,17 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let toPeerId: EnginePeer.Id let auctionContext: GiftAuctionContext let animateOut: ActionSlot> let getController: () -> ViewController? init( context: AccountContext, - toPeerId: EnginePeer.Id, auctionContext: GiftAuctionContext, animateOut: ActionSlot>, getController: @escaping () -> ViewController? ) { self.context = context - self.toPeerId = toPeerId self.auctionContext = auctionContext self.animateOut = animateOut self.getController = getController @@ -63,7 +60,6 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let averagePriceTag = GenericComponentViewTag() private let context: AccountContext - private let toPeerId: EnginePeer.Id private let auctionContext: GiftAuctionContext private let animateOut: ActionSlot> private let getController: () -> ViewController? @@ -82,13 +78,11 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { init( context: AccountContext, - toPeerId: EnginePeer.Id, auctionContext: GiftAuctionContext, animateOut: ActionSlot>, getController: @escaping () -> ViewController? ) { self.context = context - self.toPeerId = toPeerId self.auctionContext = auctionContext self.animateOut = animateOut self.getController = getController @@ -162,15 +156,16 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } - func openAuction() { - guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController else { + func proceed() { + guard let controller = self.getController() as? GiftAuctionViewScreen else { return } - self.dismiss(animated: true) - let bidController = self.context.sharedContext.makeGiftAuctionBidScreen(context: self.context, toPeerId: self.auctionContext.currentBidPeerId ?? self.toPeerId, auctionContext: self.auctionContext) - navigationController.pushViewController(bidController) + controller.completion() + +// let bidController = self.context.sharedContext.makeGiftAuctionBidScreen(context: self.context, toPeerId: self.auctionContext.currentBidPeerId ?? self.toPeerId, auctionContext: self.auctionContext) +// navigationController.pushViewController(bidController) } func openPeer(_ peer: EnginePeer, dismiss: Bool = true) { @@ -350,7 +345,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, toPeerId: self.toPeerId, auctionContext: self.auctionContext, animateOut: self.animateOut, getController: self.getController) + return State(context: self.context, auctionContext: self.auctionContext, animateOut: self.animateOut, getController: self.getController) } static var body: Body { @@ -765,7 +760,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { guard let state else { return } - state.openAuction() + state.proceed() }), availableSize: buttonSize, transition: .spring(duration: 0.2) @@ -964,16 +959,13 @@ final class GiftAuctionViewSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let toPeerId: EnginePeer.Id let auctionContext: GiftAuctionContext init( context: AccountContext, - toPeerId: EnginePeer.Id, auctionContext: GiftAuctionContext ) { self.context = context - self.toPeerId = toPeerId self.auctionContext = auctionContext } @@ -998,7 +990,6 @@ final class GiftAuctionViewSheetComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(GiftAuctionViewSheetContent( context: context.component.context, - toPeerId: context.component.toPeerId, auctionContext: context.component.auctionContext, animateOut: animateOut, getController: controller @@ -1079,16 +1070,19 @@ final class GiftAuctionViewSheetComponent: CombinedComponent { } public final class GiftAuctionViewScreen: ViewControllerComponentContainer { + fileprivate let completion: () -> Void + public init( context: AccountContext, - toPeerId: EnginePeer.Id, - auctionContext: GiftAuctionContext + auctionContext: GiftAuctionContext, + completion: @escaping () -> Void ) { + self.completion = completion + super.init( context: context, component: GiftAuctionViewSheetComponent( context: context, - toPeerId: toPeerId, auctionContext: auctionContext ), navigationBarAppearance: .none, diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 42dad06ae6..b319abb883 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -562,6 +562,7 @@ private final class SheetContent: CombinedComponent { accentColor: theme.list.itemAccentColor, value: state.amount?.value, minValue: minAmount?.value, + forceMinValue: false, allowZero: allowZero, maxValue: maxAmount?.value, placeholderText: amountPlaceholder, @@ -1296,6 +1297,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { private let textField: UITextField private let minValue: Int64 + private let forceMinValue: Bool private let allowZero: Bool private let maxValue: Int64 private let updated: (Int64) -> Void @@ -1303,11 +1305,12 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { private let animateError: () -> Void private let focusUpdated: (Bool) -> Void - init?(textField: UITextField, currency: CurrencyAmount.Currency, dateTimeFormat: PresentationDateTimeFormat, minValue: Int64, allowZero: Bool, maxValue: Int64, updated: @escaping (Int64) -> Void, isEmptyUpdated: @escaping (Bool) -> Void, animateError: @escaping () -> Void, focusUpdated: @escaping (Bool) -> Void) { + init?(textField: UITextField, currency: CurrencyAmount.Currency, dateTimeFormat: PresentationDateTimeFormat, minValue: Int64, forceMinValue: Bool, allowZero: Bool, maxValue: Int64, updated: @escaping (Int64) -> Void, isEmptyUpdated: @escaping (Bool) -> Void, animateError: @escaping () -> Void, focusUpdated: @escaping (Bool) -> Void) { self.textField = textField self.currency = currency self.dateTimeFormat = dateTimeFormat self.minValue = minValue + self.forceMinValue = forceMinValue self.allowZero = allowZero self.maxValue = maxValue self.updated = updated @@ -1434,7 +1437,17 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { } let amount: Int64 = self.amountFrom(text: newText) - if amount > self.maxValue { + if self.forceMinValue && amount < self.minValue { + switch self.currency { + case .stars: + textField.text = "\(self.minValue)" + case .ton: + textField.text = "\(formatTonAmountText(self.minValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: self.dateTimeFormat.decimalSeparator, groupingSeparator: ""), maxDecimalPositions: nil))" + } + self.onTextChanged(text: self.textField.text ?? "") + self.animateError() + return false + } else if amount > self.maxValue { switch self.currency { case .stars: textField.text = "\(self.maxValue)" @@ -1452,8 +1465,8 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { } } -private final class AmountFieldComponent: Component { - typealias EnvironmentType = Empty +public final class AmountFieldComponent: Component { + public typealias EnvironmentType = Empty let textColor: UIColor let secondaryColor: UIColor @@ -1461,25 +1474,29 @@ private final class AmountFieldComponent: Component { let accentColor: UIColor let value: Int64? let minValue: Int64? + let forceMinValue: Bool let allowZero: Bool let maxValue: Int64? let placeholderText: String + let textFieldOffset: CGPoint let labelText: String? let currency: CurrencyAmount.Currency let dateTimeFormat: PresentationDateTimeFormat let amountUpdated: (Int64?) -> Void let tag: AnyObject? - init( + public init( textColor: UIColor, secondaryColor: UIColor, placeholderColor: UIColor, accentColor: UIColor, value: Int64?, minValue: Int64?, + forceMinValue: Bool, allowZero: Bool, maxValue: Int64?, placeholderText: String, + textFieldOffset: CGPoint = .zero, labelText: String?, currency: CurrencyAmount.Currency, dateTimeFormat: PresentationDateTimeFormat, @@ -1492,9 +1509,11 @@ private final class AmountFieldComponent: Component { self.accentColor = accentColor self.value = value self.minValue = minValue + self.forceMinValue = forceMinValue self.allowZero = allowZero self.maxValue = maxValue self.placeholderText = placeholderText + self.textFieldOffset = textFieldOffset self.labelText = labelText self.currency = currency self.dateTimeFormat = dateTimeFormat @@ -1502,7 +1521,7 @@ private final class AmountFieldComponent: Component { self.tag = tag } - static func ==(lhs: AmountFieldComponent, rhs: AmountFieldComponent) -> Bool { + public static func ==(lhs: AmountFieldComponent, rhs: AmountFieldComponent) -> Bool { if lhs.textColor != rhs.textColor { return false } @@ -1539,7 +1558,7 @@ private final class AmountFieldComponent: Component { return true } - final class View: UIView, UITextFieldDelegate, ComponentTaggedView { + public final class View: UIView, UITextFieldDelegate, ComponentTaggedView { public func matches(tag: Any) -> Bool { if let component = self.component, let componentTag = component.tag { let tag = tag as AnyObject @@ -1563,7 +1582,7 @@ private final class AmountFieldComponent: Component { private var didSetValueOnce = false - override init(frame: CGRect) { + public override init(frame: CGRect) { self.placeholderView = ComponentView() self.textField = TextFieldNodeView(frame: .zero) self.labelView = ComponentView() @@ -1577,15 +1596,19 @@ private final class AmountFieldComponent: Component { fatalError("init(coder:) has not been implemented") } - func activateInput() { + public func activateInput() { self.textField.becomeFirstResponder() } - func selectAll() { - self.textField.selectAll(nil) + public func deactivateInput() { + self.textField.resignFirstResponder() } - func animateError() { + public func selectAll() { + self.textField.selectAll(nil) + } + + public func animateError() { self.textField.layer.addShakeAnimation() let hapticFeedback = HapticFeedback() hapticFeedback.error() @@ -1594,6 +1617,13 @@ private final class AmountFieldComponent: Component { }) } + public func resetValue() { + guard let component = self.component, let value = component.value else { + return + } + self.textField.text = "\(value)" + } + func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -1633,6 +1663,7 @@ private final class AmountFieldComponent: Component { currency: component.currency, dateTimeFormat: component.dateTimeFormat, minValue: component.minValue ?? 0, + forceMinValue: component.forceMinValue, allowZero: component.allowZero, maxValue: component.maxValue ?? Int64.max, updated: { [weak self] value in @@ -1669,6 +1700,7 @@ private final class AmountFieldComponent: Component { currency: component.currency, dateTimeFormat: component.dateTimeFormat, minValue: component.minValue ?? 0, + forceMinValue: component.forceMinValue, allowZero: component.allowZero, maxValue: component.maxValue ?? 10000000, updated: { [weak self] value in @@ -1790,7 +1822,7 @@ private final class AmountFieldComponent: Component { labelView.removeFromSuperview() } - self.textField.frame = CGRect(x: leftInset, y: 4.0, width: size.width - 30.0, height: 44.0) + self.textField.frame = CGRect(x: leftInset + component.textFieldOffset.x, y: 4.0 + component.textFieldOffset.y, width: size.width - 30.0, height: 44.0) return size } @@ -1805,28 +1837,6 @@ private final class AmountFieldComponent: Component { } } - -func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(foregroundColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} - private struct StarsWithdrawConfiguration { static var defaultValue: StarsWithdrawConfiguration { return StarsWithdrawConfiguration(minWithdrawAmount: nil, maxPaidMediaAmount: nil, usdWithdrawRate: nil, tonUsdRate: nil) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index df0b3cfcc9..35ef3951d6 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -37,6 +37,7 @@ import TelegramStringFormatting import TextFormat import BrowserUI import MediaEditorScreen +import GiftSetupScreen private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -1498,19 +1499,30 @@ func openResolvedUrlImpl( present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } case let .auction(auctionContext): - if let auctionContext { + if let auctionContext, case let .generic(gift) = auctionContext.gift { if let currentBidPeerId = auctionContext.currentBidPeerId { let controller = context.sharedContext.makeGiftAuctionBidScreen( context: context, toPeerId: currentBidPeerId, + text: nil, + entities: nil, + hideName: false, auctionContext: auctionContext ) navigationController?.pushViewController(controller) } else { let controller = context.sharedContext.makeGiftAuctionViewScreen( context: context, - toPeerId: context.account.peerId, - auctionContext: auctionContext + auctionContext: auctionContext, + completion: { [weak navigationController] in + let controller = GiftSetupScreen( + context: context, + peerId: context.account.peerId, + subject: .starGift(gift, nil), + completion: nil + ) + navigationController?.pushViewController(controller) + } ) navigationController?.pushViewController(controller) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 7bdccb5ed0..d7ba9e2d6b 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3846,12 +3846,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftAuctionInfoScreen(context: context, auctionContext: auctionContext, completion: completion) } - public func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, auctionContext: GiftAuctionContext) -> ViewController { - return GiftAuctionBidScreen(context: context, toPeerId: toPeerId, auctionContext: auctionContext) + public func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext) -> ViewController { + return GiftAuctionBidScreen(context: context, toPeerId: toPeerId, text: text, entities: entities, hideName: hideName, auctionContext: auctionContext) } - public func makeGiftAuctionViewScreen(context: AccountContext, toPeerId: EnginePeer.Id, auctionContext: GiftAuctionContext) -> ViewController { - return GiftAuctionViewScreen(context: context, toPeerId: toPeerId, auctionContext: auctionContext) + public func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping () -> Void) -> ViewController { + return GiftAuctionViewScreen(context: context, auctionContext: auctionContext, completion: completion) } public func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController {