diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 39da024bab..2a1e6d8e10 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13008,6 +13008,8 @@ Sorry for the inconvenience."; "Gift.Options.Gift.Filter.AllGifts" = "All Gifts"; "Gift.Options.Gift.Filter.Limited" = "Limited"; "Gift.Options.Gift.Limited" = "Limited"; +"Gift.Options.Gift.SoldOut" = "Sold Out"; +"Gift.Options.SoldOut.Text" = "Sorry, this gift is sold out."; "PeerInfo.PaneGifts" = "Gifts"; @@ -13039,6 +13041,10 @@ Sorry for the inconvenience."; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.Send" = "Send a Gift for"; "Gift.Send.Limited" = "Limited"; +"Gift.Send.Left" = "left"; + +"Gift.Send.ErrorUnknown" = "An error occurred. Please try again."; +"Gift.Send.ErrorOutOfStock" = "Sorry, this gift is sold out. Please choose another gift."; "Profile.SendGift" = "Send a Gift"; "Settings.SendGift" = "Send a Gift"; @@ -13061,6 +13067,7 @@ Sorry for the inconvenience."; "Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium"; "Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium"; +"Notification.PremiumGift.More" = "more"; "Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features."; diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 3fcfba0dd7..d4870d516a 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -931,13 +931,13 @@ public final class AvatarNode: ASDisplayNode { if let repliesIcon = repliesIcon { context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size)) } - } else if case .anonymousSavedMessagesIcon = parameters.icon { + } else if case let .anonymousSavedMessagesIcon(isColored) = parameters.icon { let factor = bounds.size.width / 60.0 context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.scaleBy(x: factor, y: -factor) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) - if let theme = parameters.theme, theme.overallDarkAppearance { + if let theme = parameters.theme, theme.overallDarkAppearance, !isColored { if let anonymousSavedMessagesDarkIcon = anonymousSavedMessagesDarkIcon { context.draw(anonymousSavedMessagesDarkIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesDarkIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesDarkIcon.size.height) / 2.0)), size: anonymousSavedMessagesDarkIcon.size)) } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 6392b0de64..cbf8994a0f 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -1558,19 +1558,23 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz applePayController.presentingViewController?.dismiss(animated: true, completion: nil) } - let text: String + let text: String? switch error { - case .precheckoutFailed: - text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed - case .paymentFailed: - text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed - case .alreadyPaid: - text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid - case .generic: - text = strongSelf.presentationData.strings.Checkout_ErrorGeneric + case .precheckoutFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed + case .paymentFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed + case .alreadyPaid: + text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid + case .generic: + text = strongSelf.presentationData.strings.Checkout_ErrorGeneric + default: + text = nil } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + if let text { + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + } strongSelf.failed() } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 1296173518..a00e0e0c84 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -588,6 +588,7 @@ public enum SendBotPaymentFormError { case precheckoutFailed case paymentFailed case alreadyPaid + case starGiftOutOfStock } public enum SendBotPaymentResult { @@ -702,6 +703,8 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa return .fail(.paymentFailed) } else if error.errorDescription == "INVOICE_ALREADY_PAID" { return .fail(.alreadyPaid) + } else if error.errorDescription == "STARGIFT_USAGE_LIMITED" { + return .fail(.starGiftOutOfStock) } return .fail(.generic) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index ed93377cda..601a067d4b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -6224,7 +6224,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } for contentNode in self.contentNodes { - if contentNode is ChatMessageMediaBubbleContentNode { + if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode { contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) } else { contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index eecab9955f..41b478eb1e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -50,6 +50,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let buttonStarsNode: PremiumStarsNode private let buttonTitleNode: TextNode + private let moreTextNode: TextNode + private var maskView: UIImageView? private var maskOverlayView: UIView? @@ -61,6 +63,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var isExpanded: Bool = false private var appliedIsExpanded: Bool = false + private var isStarGift = false + private var currentProgressDisposable: Disposable? override public var visibility: ListViewItemNodeVisibility { @@ -138,6 +142,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.ribbonTextNode.isUserInteractionEnabled = false self.ribbonTextNode.displaysAsynchronously = false + self.moreTextNode = TextNode() + self.moreTextNode.isUserInteractionEnabled = false + self.moreTextNode.displaysAsynchronously = false + super.init() self.addSubnode(self.labelNode) @@ -147,6 +155,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.textClippingNode.addSubnode(self.subtitleNode.textNode) self.addSubnode(self.placeholderNode) self.addSubnode(self.animationNode) + self.addSubnode(self.moreTextNode) self.addSubnode(self.buttonNode) self.buttonNode.addSubnode(self.buttonStarsNode) @@ -183,6 +192,19 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.currentProgressDisposable?.dispose() } + override public func didLoad() { + super.didLoad() + + self.maskView = UIImageView() + + let maskOverlayView = UIView() + maskOverlayView.alpha = 0.0 + maskOverlayView.backgroundColor = .white + self.maskOverlayView = maskOverlayView + + self.maskView?.addSubview(maskOverlayView) + } + @objc private func buttonPressed() { guard let item = self.item else { return @@ -190,6 +212,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default, progress: self.makeProgress())) } + private func expandPressed() { + self.isExpanded = !self.isExpanded + guard let item = self.item else{ + return + } + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + } + private func makeProgress() -> Promise { let progress = Promise() self.currentProgressDisposable?.dispose() @@ -263,6 +293,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) let makeMeasureTextLayout = TextNode.asyncLayout(nil) + let makeMoreTextLayout = TextNode.asyncLayout(self.moreTextNode) let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage @@ -290,6 +321,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var ribbonTitle = "" var hasServiceMessage = true var textSpacing: CGFloat = 0.0 + var isStarGift = false for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { @@ -377,6 +409,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { hasServiceMessage = false } case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted): + isStarGift = true let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" title = item.presentationData.strings.Notification_StarGift_Title(authorName).string if let giftText, !giftText.isEmpty { @@ -439,8 +472,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (moreLayout, moreApply) = makeMoreTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_More, font: Font.semibold(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let attributedText: NSAttributedString - if let _ = animationFile { + if !entities.isEmpty { attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.monospace(13.0), blockQuoteFont: Font.regular(13.0), message: nil) } else { attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( @@ -578,6 +613,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } strongSelf.item = item + strongSelf.isStarGift = isStarGift strongSelf.updateVisibility() @@ -599,6 +635,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { )) let _ = buttonTitleApply() let _ = ribbonTextApply() + let _ = moreApply() let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame @@ -606,7 +643,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame - let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: boundingWidth, height: clippedTextHeight)) + let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight)) let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size) @@ -617,10 +654,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil) } if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView { - animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) - animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) + animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil) + animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil) } - + animation.animator.updateFrame(layer: strongSelf.moreTextNode.layer, frame: CGRect(origin: CGPoint(x: clippingTextFrame.maxX - moreLayout.size.width, y: clippingTextFrame.maxY - moreLayout.size.height), size: moreLayout.size), completion: nil) if !subtitleLayout.spoilers.isEmpty { let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText @@ -734,24 +771,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.updateAbsoluteRect(rect, within: size) } - if canExpand { - if strongSelf.maskView?.image == nil { - strongSelf.maskView?.image = generateMaskImage() + if canExpand, let maskView = strongSelf.maskView { + if maskView.image == nil { + maskView.image = generateMaskImage() } strongSelf.textClippingNode.view.mask = strongSelf.maskView -// var expandIconFrame: CGRect = .zero -// if let icon = strongSelf.expandIcon.image { -// expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size) -// if wasHidden || isFirstTime { -// strongSelf.expandIcon.position = expandIconFrame.center -// } else { -// animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil) -// } -// strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size) -// } + animation.animator.updateAlpha(layer: strongSelf.moreTextNode.layer, alpha: strongSelf.isExpanded ? 0.0 : 1.0, completion: nil) } else { strongSelf.textClippingNode.view.mask = nil + strongSelf.moreTextNode.alpha = 0.0 } switch strongSelf.visibility { @@ -876,6 +905,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if self.buttonNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .ignore) + } else if self.textClippingNode.frame.contains(point) && !self.isExpanded && !self.moreTextNode.alpha.isZero { + self.expandPressed() + return ChatMessageBubbleContentTapAction(content: .ignore) } else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .openMessage) } else if self.mediaBackgroundContent?.frame.contains(point) == true { @@ -934,7 +966,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if !alreadySeen && self.animationNode.isPlaying { item.controllerInteraction.playNextOutgoingGift = false - Queue.mainQueue().after(1.0) { + + Queue.mainQueue().after(self.isStarGift ? 0.1 : 1.0) { item.controllerInteraction.animateDiceSuccess(false, true) } } @@ -943,7 +976,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } private func generateMaskImage() -> UIImage? { - return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in + return generateImage(CGSize(width: 100.0, height: 30.0), rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) context.setFillColor(UIColor.white.cgColor) @@ -956,7 +989,7 @@ private func generateMaskImage() -> UIImage? { let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.setBlendMode(.copy) - context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0))) - context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) - })?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0)) + context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 12.0), size: CGSize(width: 130.0, height: 18.0))) + context.drawLinearGradient(gradient, start: CGPoint(x: 30.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 18.0, right: 70.0)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 2f1b3612be..fda1f35734 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -64,6 +64,7 @@ public final class GiftItemComponent: Component { let ribbon: Ribbon? let isLoading: Bool let isHidden: Bool + let isSoldOut: Bool public init( context: AccountContext, @@ -75,7 +76,8 @@ public final class GiftItemComponent: Component { price: String, ribbon: Ribbon? = nil, isLoading: Bool = false, - isHidden: Bool = false + isHidden: Bool = false, + isSoldOut: Bool = false ) { self.context = context self.theme = theme @@ -87,6 +89,7 @@ public final class GiftItemComponent: Component { self.ribbon = ribbon self.isLoading = isLoading self.isHidden = isHidden + self.isSoldOut = isSoldOut } public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool { @@ -120,6 +123,9 @@ public final class GiftItemComponent: Component { if lhs.isHidden != rhs.isHidden { return false } + if lhs.isSoldOut != rhs.isSoldOut { + return false + } return true } @@ -284,14 +290,26 @@ public final class GiftItemComponent: Component { } } + let buttonColor: UIColor + var isStars = false + if component.isSoldOut { + buttonColor = component.theme.list.itemDestructiveColor + } else if component.price.containsEmoji { + buttonColor = UIColor(rgb: 0xd3720a) + isStars = true + } else { + buttonColor = component.theme.list.itemAccentColor + } + let buttonSize = self.button.update( transition: transition, component: AnyComponent( ButtonContentComponent( context: component.context, text: component.price, - color: component.price.containsEmoji ? UIColor(rgb: 0xd3720a) : component.theme.list.itemAccentColor, - isStars: component.price.containsEmoji) + color: buttonColor, + isStars: isStars + ) ), environment: {}, containerSize: availableSize diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index f7c9b5873b..4171c761bc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -25,6 +25,7 @@ import GiftItemComponent import InAppPurchaseManager import TabSelectorComponent import GiftSetupScreen +import UndoUI final class GiftOptionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -173,6 +174,20 @@ final class GiftOptionsScreenComponent: Component { self.updateScrolling(transition: .immediate) } + private func dismissAllTooltips(controller: ViewController) { + controller.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + }) + controller.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + } + private func updateScrolling(transition: ComponentTransition) { guard let environment = self.environment, let component = self.component else { return @@ -274,6 +289,7 @@ final class GiftOptionsScreenComponent: Component { self.starsItems[itemId] = visibleItem } + let isSoldOut = gift.availability?.remains == 0 let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -284,13 +300,14 @@ final class GiftOptionsScreenComponent: Component { theme: environment.theme, peer: nil, subject: .starGift(gift.id, gift.file), - price: "⭐️ \(gift.price)", + price: isSoldOut ? environment.strings.Gift_Options_Gift_SoldOut : "⭐️ \(gift.price)", ribbon: gift.availability != nil ? GiftItemComponent.Ribbon( text: environment.strings.Gift_Options_Gift_Limited, color: .blue ) - : nil + : nil, + isSoldOut: isSoldOut ) ), effectAlignment: .center, @@ -303,13 +320,25 @@ final class GiftOptionsScreenComponent: Component { } else { mainController = controller } - let giftController = GiftSetupScreen( - context: component.context, - peerId: component.peerId, - subject: .starGift(gift), - completion: component.completion - ) - mainController.push(giftController) + if gift.availability?.remains == 0 { + self.dismissAllTooltips(controller: mainController) + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: component.context, file: gift.file, loop: false, title: nil, text: presentationData.strings.Gift_Options_SoldOut_Text, undoText: nil, customAction: nil), + elevatedLayout: false, + action: { _ in return true } + ) + mainController.present(resultController, in: .window(.root)) + } else { + let giftController = GiftSetupScreen( + context: component.context, + peerId: component.peerId, + subject: .starGift(gift), + completion: component.completion + ) + mainController.push(giftController) + } } } }, diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD index a5a8867320..82507f195a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -43,6 +43,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/InAppPurchaseManager", + "//submodules/Components/BlurredBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index 12a43a1017..4ec4cfbe93 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -258,7 +258,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { apply().1(ListViewItemApply(isOnScreen: true)) }) itemNode!.isUserInteractionEnabled = false - itemNode?.visibility = .visible(1.0, .infinite) + itemNode!.visibility = .visible(1.0, .infinite) messageNodes.append(itemNode!) self.initialBubbleHeight = itemNode?.frame.height diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 713deadceb..a91459fc09 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -29,6 +29,7 @@ import ChatPresentationInterfaceState import AudioToolbox import TextFormat import InAppPurchaseManager +import BlurredBackgroundComponent final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -78,6 +79,9 @@ final class GiftSetupScreenComponent: Component { private let introContent = ComponentView() private let introSection = ComponentView() private let hideSection = ComponentView() + + private let buttonBackground = ComponentView() + private let buttonSeparator = SimpleLayer() private let button = ComponentView() private var ignoreScrolling: Bool = false @@ -356,6 +360,27 @@ final class GiftSetupScreenComponent: Component { } starsContext.load(force: true) + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + + self.inProgress = false + self.state?.updated() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + var errorText: String? + switch error { + case .starGiftOutOfStock: + errorText = presentationData.strings.Gift_Send_ErrorOutOfStock + default: + errorText = presentationData.strings.Gift_Send_ErrorUnknown + } + + if let errorText = errorText { + let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller.present(alertController, in: .window(.root)) + } }) }) } @@ -826,6 +851,34 @@ final class GiftSetupScreenComponent: Component { self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) } + let buttonHeight: CGFloat = 50.0 + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset + + let bottomPanelAlpha: CGFloat = 1.0 + let bottomPanelSize = self.buttonBackground.update( + transition: transition, + component: AnyComponent(BlurredBackgroundComponent( + color: environment.theme.rootController.tabBar.backgroundColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight) + ) + self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor + self.buttonSeparator.opacity = Float(bottomPanelAlpha) + + if let view = self.buttonBackground.view { + if view.superview == nil { + self.addSubview(view) + self.layer.addSublayer(self.buttonSeparator) + } + view.alpha = bottomPanelAlpha + view.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: bottomPanelSize) + self.buttonSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: CGSize(width: availableSize.width, height: UIScreenPixel)) + } + + var buttonIsEnabled = true let buttonString: String switch component.subject { case let .premium(product): @@ -834,6 +887,9 @@ final class GiftSetupScreenComponent: Component { case let .starGift(starGift): let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator) buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" + if let availability = starGift.availability, availability.remains == 0 { + buttonIsEnabled = false + } } let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) @@ -856,20 +912,20 @@ final class GiftSetupScreenComponent: Component { id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), - isEnabled: true, + isEnabled: buttonIsEnabled, displaysProgress: self.inProgress, action: { [weak self] in self?.proceed() } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) ) if let buttonView = self.button.view { if buttonView.superview == nil { self.addSubview(buttonView) } - buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize) + buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize) } if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift index 267382cf3e..bb1ab4199c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift @@ -119,6 +119,7 @@ public class RemainingCountComponent: Component { private let badgeForeground: SimpleLayer private let badgeLabel: BadgeLabelView + private let badgeLeftLabel = ComponentView() private let badgeLabelMaskView = UIImageView() private var badgeTailPosition: CGFloat = 0.0 @@ -737,7 +738,6 @@ final class BadgeLabelView: UIView { } private var itemViews: [Int: StackView] = [:] - private var staticLabel = UILabel() init() { super.init(frame: .zero) @@ -752,7 +752,6 @@ final class BadgeLabelView: UIView { var color: UIColor = .white { didSet { - self.staticLabel.textColor = self.color for (_, view) in self.itemViews { view.color = self.color } @@ -760,25 +759,6 @@ final class BadgeLabelView: UIView { } func update(value: String, transition: ComponentTransition) -> CGSize { - if value.contains(" ") { - for (_, view) in self.itemViews { - view.isHidden = true - } - - if self.staticLabel.superview == nil { - self.staticLabel.textColor = self.color - self.staticLabel.font = font - - self.addSubview(self.staticLabel) - } - - self.staticLabel.text = value - let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0)) - self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight)) - - return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height)) - } - let string = value let stringArray = Array(string.map { String($0) }.reversed())