diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d19b9a7112..7ce787238b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15434,3 +15434,35 @@ Error: %8$@"; "LiveStream.ErrorMaxAllowedEmoji.Text_any" = "You can send up to %d emoji."; "Stars.Purchase.StarGiftOfferInfo" = "Buy Stars to make a gift buy offer."; + +"Notification.StarsGiftOffer.Offer" = "%1$@ offered you %2$@ for your gift %3$@"; +"Notification.StarsGiftOffer.Offer.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.Offer.Stars_any" = "%@ Stars"; +"Notification.StarsGiftOffer.OfferYou" = "You offered %1$@ %2$@ for their gift %3$@"; +"Notification.StarsGiftOffer.OfferYou.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.OfferYou.Stars_any" = "%@ Stars"; + +"Notification.StarsGiftOffer.Accepted" = "%1$@ accepted your offer and sold you %2$@ for %3$@"; +"Notification.StarsGiftOffer.Accepted.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.Accepted.Stars_any" = "%@ Stars"; +"Notification.StarsGiftOffer.AcceptedYou" = "You sold %1$@ to %2$@ for %3$@"; +"Notification.StarsGiftOffer.AcceptedYou.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.AcceptedYou.Stars_any" = "%@ Stars"; + +"Notification.StarsGiftOffer.Rejected" = "%1$@ rejected your offer for %2$@ – your %3$@ have been refunded"; +"Notification.StarsGiftOffer.Rejected.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.Rejected.Stars_any" = "%@ Stars"; +"Notification.StarsGiftOffer.RejectedYou" = "You rejected %1$@'s offer"; +"Notification.StarsGiftOffer.RejectedYou.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.RejectedYou.Stars_any" = "%@ Stars"; + +"Notification.StarsGiftOffer.Expired" = "%1$@ didn't respond to your offer for %2$@ within %3$@ – your %4$@ have been refunded"; +"Notification.StarsGiftOffer.Expired.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.Expired.Stars_any" = "%@ Stars"; +"Notification.StarsGiftOffer.ExpiredYou" = "The offer from %1$@ to buy your %2$@ for %3$@ has expired"; +"Notification.StarsGiftOffer.ExpiredYou.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.ExpiredYou.Stars_any" = "%@ Stars"; + +"CreateExternalStream.RevokeStreamKey" = "Revoke Stream Key"; +"CreateExternalStream.Revoke.Text" = "Do you want to revoke the stream key?"; +"CreateExternalStream.Revoke.Revoke" = "Revoke"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5b2029e260..1a1aa391b2 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1427,6 +1427,7 @@ public protocol SharedAccountContext: AnyObject { func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?) -> ViewController func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void) -> ViewController func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController + func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift index 1da8fd8434..e970e67688 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift @@ -13,14 +13,14 @@ enum BotCheckoutActionButtonState: Equatable { private let titleFont = Font.semibold(17.0) -final class BotCheckoutActionButton: HighlightableButtonNode { +final class BotCheckoutActionButton: HighlightTrackingButtonNode { static var height: CGFloat = 52.0 private var activeFillColor: UIColor private var inactiveFillColor: UIColor private var foregroundColor: UIColor - private let activeBackgroundNode: ASImageNode + private let activeBackgroundNode: ASDisplayNode private var applePayButton: UIButton? private let labelNode: TextNode @@ -29,23 +29,17 @@ final class BotCheckoutActionButton: HighlightableButtonNode { private var placeholderNode: ShimmerEffectNode? - private var activeImage: UIImage? - private var inactiveImage: UIImage? - init(activeFillColor: UIColor, inactiveFillColor: UIColor, foregroundColor: UIColor) { self.activeFillColor = activeFillColor self.inactiveFillColor = inactiveFillColor self.foregroundColor = foregroundColor - let diameter: CGFloat = 20.0 - self.activeImage = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor) - self.inactiveImage = generateStretchableFilledCircleImage(diameter: diameter, color: inactiveFillColor) + let diameter: CGFloat = 52.0 - self.activeBackgroundNode = ASImageNode() - self.activeBackgroundNode.displaysAsynchronously = false - self.activeBackgroundNode.displayWithoutProcessing = true + self.activeBackgroundNode = ASDisplayNode() self.activeBackgroundNode.isLayerBacked = true - self.activeBackgroundNode.image = self.activeImage + self.activeBackgroundNode.backgroundColor = activeFillColor + self.activeBackgroundNode.cornerRadius = diameter / 2.0 self.labelNode = TextNode() self.labelNode.displaysAsynchronously = false @@ -55,6 +49,21 @@ final class BotCheckoutActionButton: HighlightableButtonNode { self.addSubnode(self.activeBackgroundNode) self.addSubnode(self.labelNode) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + let transition = ContainedViewLayoutTransition.animated(duration: highlighted ? 0.25 : 0.35, curve: .spring) + if highlighted { + let highlightedColor = self.activeFillColor.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01) + transition.updateBackgroundColor(node: self.activeBackgroundNode, color: highlightedColor) + transition.updateTransformScale(node: self, scale: 1.05) + } else { + transition.updateBackgroundColor(node: self.activeBackgroundNode, color: self.activeFillColor) + transition.updateTransformScale(node: self, scale: 1.0) + } + } } func setState(_ state: BotCheckoutActionButtonState) { @@ -93,14 +102,8 @@ final class BotCheckoutActionButton: HighlightableButtonNode { placeholderNode.removeFromSupernode() } - let image = isEnabled ? self.activeImage : self.inactiveImage - if let image = image, let currentImage = self.activeBackgroundNode.image, currentImage !== image { - self.activeBackgroundNode.image = image - self.activeBackgroundNode.layer.animate(from: currentImage.cgImage! as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) - } else { - self.activeBackgroundNode.image = image - } - + self.activeBackgroundNode.backgroundColor = isEnabled ? self.activeFillColor : self.inactiveFillColor + let makeLayout = TextNode.asyncLayout(self.labelNode) let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 191f9b27c8..e81bf2e2a4 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -259,31 +259,31 @@ enum BotCheckoutEntry: ItemListNodeEntry { } }) case let .paymentMethod(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openPaymentMethod() }) case let .shippingInfo(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.address(.street1)) }) case let .shippingMethod(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openShippingMethod() }) case let .nameInfo(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.name) }) case let .emailInfo(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.email) }) case let .phoneInfo(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.phone) }) case let .actionPlaceholder(_, shimmeringIndex): - return ItemListDisclosureItem(presentationData: presentationData, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { }, shimmeringIndex: shimmeringIndex) } } @@ -1245,7 +1245,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { var updatedInsets = layout.intrinsicInsets - let bottomPanelHorizontalInset: CGFloat = 16.0 + let bottomPanelHorizontalInset: CGFloat = 30.0 var botName: String? if let botPeer = self.botPeerValue { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift index 3104afe921..c2c4a9c922 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift @@ -270,7 +270,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: params.width, height: separatorHeight)) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift index 745aa4162f..a88ecb09ed 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift @@ -141,13 +141,13 @@ class BotCheckoutPriceItemNode: ListViewItemNode { let naturalContentHeight: CGFloat var verticalOffset: CGFloat = 0.0 if item.isFinal { - naturalContentHeight = 44.0 + naturalContentHeight = 52.0 } else { switch neighbors.bottom { case .otherSection, .none: - naturalContentHeight = 44.0 + naturalContentHeight = 52.0 default: - naturalContentHeight = 34.0 + naturalContentHeight = 42.0 } } if let _ = previousItem as? BotCheckoutHeaderItem { @@ -209,7 +209,7 @@ class BotCheckoutPriceItemNode: ListViewItemNode { strongSelf.separatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel)) diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift index dfba43615a..5d89bef1e7 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift @@ -70,15 +70,17 @@ final class BotPaymentActionItemNode: BotPaymentItemNode { self.titleNode.attributedText = NSAttributedString(string: self.title, font: titleFont, textColor: theme.list.itemAccentColor) } - self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: 44.0)) - transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0 + UIScreenPixel))) + let height: CGFloat = 52.0 + + self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: height)) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height + UIScreenPixel))) let leftInset: CGFloat = 16.0 let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 32.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize)) - return 44.0 + return height } @objc func buttonPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift index f41dda6306..0e376a994e 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift @@ -45,9 +45,9 @@ final class BotPaymentCardInputItemNode: BotPaymentItemNode, STPPaymentCardTextF self.cardField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance } - self.cardField.frame = CGRect(origin: CGPoint(x: 5.0 + sideInset, y: 0.0), size: CGSize(width: width - 10.0 - sideInset * 2.0, height: 44.0)) + self.cardField.frame = CGRect(origin: CGPoint(x: 5.0 + sideInset, y: 4.0), size: CGSize(width: width - 10.0 - sideInset * 2.0, height: 44.0)) - return 44.0 + return 52.0 } func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) { diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift index b482f85fea..6182eafb47 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift @@ -102,13 +102,15 @@ class BotPaymentDisclosureItemNode: BotPaymentItemNode { } } - self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: 44.0)) - transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0 + UIScreenPixel))) + let height: CGFloat = 52.0 + + self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: height)) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height + UIScreenPixel))) let leftInset: CGFloat = 16.0 let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize)) var textInset = leftInset if !titleSize.width.isZero { @@ -117,9 +119,9 @@ class BotPaymentDisclosureItemNode: BotPaymentItemNode { textInset = max(measuredInset, textInset) let textSize = self.textNode.measure(CGSize(width: width - measuredInset - 8.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 11.0), size: textSize)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 16.0), size: textSize)) - return 44.0 + return height } @objc func buttonPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift index 41150436ee..eb3404884c 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift @@ -108,11 +108,12 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate { self.textField.textField.tintColor = theme.list.itemAccentColor } + let height: CGFloat = 52.0 let leftInset: CGFloat = 16.0 let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize)) var textInset = leftInset if !titleSize.width.isZero { @@ -121,9 +122,9 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate { textInset = max(measuredInset, textInset) - transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 0.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0))) + transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 5.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0))) - return 44.0 + return height } func activateInput() { diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift index 38d57cefd4..15a03e7c97 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift @@ -74,7 +74,7 @@ class BotPaymentItemNode: ASDisplayNode { self.bottomSeparatorNode.isHidden = hasCorners } - self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))) transition.updateFrame(node: self.maskNode, frame: self.backgroundNode.frame.insetBy(dx: sideInset, dy: 0.0)) diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift index 5f582bfb9c..cffd44d2e9 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift @@ -74,14 +74,14 @@ final class BotPaymentSwitchItemNode: BotPaymentItemNode { let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize)) let switchSize = self.switchNode.measure(CGSize(width: 100.0, height: 100.0)) - let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - sideInset, y: 6.0), size: switchSize) + let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - sideInset, y: 12.0), size: switchSize) transition.updateFrame(node: self.switchNode, frame: switchFrame) transition.updateFrame(node: self.buttonNode, frame: switchFrame) - return 44.0 + return 52.0 } @objc private func buttonPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift index 6100325d62..1d464bdcdf 100644 --- a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift @@ -335,7 +335,7 @@ final class BotReceiptControllerNode: ItemListControllerNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { var updatedInsets = layout.intrinsicInsets - let bottomPanelHorizontalInset: CGFloat = 16.0 + let bottomPanelHorizontalInset: CGFloat = 30.0 let bottomPanelVerticalInset: CGFloat = 16.0 let bottomPanelHeight = max(updatedInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 8fc6c4d6ce..20a3dbc59a 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -1425,9 +1425,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU var nodeList = document.getElementsByTagName('link'); for (var i = 0; i < nodeList.length; i++) { - if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')||(nodeList[i].getAttribute('rel').startsWith('apple-touch-icon'))) - { - const node = nodeList[i]; + var rel = nodeList[i].getAttribute('rel') || ''; + if ( + rel === 'icon' || + rel === 'shortcut icon' || + rel.indexOf('apple-touch-icon') === 0 + ) { + var node = nodeList[i]; favicons.push({ url: node.getAttribute('href'), sizes: node.getAttribute('sizes') diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 94b2a3a2ba..2979b04e82 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -157,40 +157,6 @@ public final class SheetComponent: C } } - final class BackgroundView: UIView { - let topCornersView = UIView() - let bottomCornersView = UIView() - - override init(frame: CGRect) { - super.init(frame: frame) - - self.topCornersView.clipsToBounds = true - self.topCornersView.layer.cornerCurve = .continuous - self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - - self.bottomCornersView.clipsToBounds = true - self.bottomCornersView.layer.cornerCurve = .continuous - self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - - self.addSubview(self.topCornersView) - self.topCornersView.addSubview(self.bottomCornersView) - } - - required init?(coder: NSCoder) { - preconditionFailure() - } - - func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) { - transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius) - transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius) - - transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size)) - transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size)) - - transition.setBackgroundColor(view: self.bottomCornersView, color: color) - } - } - public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { public final class Tag { public init() { @@ -208,9 +174,9 @@ public final class SheetComponent: C private let dimView: UIView private let scrollView: ScrollView - private let backgroundView: BackgroundView + private let backgroundView: SheetBackgroundView private var effectView: UIVisualEffectView? - private let clipView: BackgroundView + private let clipView: SheetBackgroundView private let contentView: ComponentView private var headerView: ComponentView? @@ -233,8 +199,8 @@ public final class SheetComponent: C self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceVertical = true - self.backgroundView = BackgroundView() - self.clipView = BackgroundView() + self.backgroundView = SheetBackgroundView() + self.clipView = SheetBackgroundView() self.contentView = ComponentView() @@ -589,3 +555,37 @@ public final class SheetComponent: C return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public final class SheetBackgroundView: UIView { + let topCornersView = UIView() + let bottomCornersView = UIView() + + public override init(frame: CGRect) { + super.init(frame: frame) + + self.topCornersView.clipsToBounds = true + self.topCornersView.layer.cornerCurve = .continuous + self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + self.bottomCornersView.clipsToBounds = true + self.bottomCornersView.layer.cornerCurve = .continuous + self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + + self.addSubview(self.topCornersView) + self.topCornersView.addSubview(self.bottomCornersView) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + public func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) { + transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius) + transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius) + + transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size)) + transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size)) + + transition.setBackgroundColor(view: self.bottomCornersView, color: color) + } +} diff --git a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift index 1b865e41ea..a6c32595ea 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift @@ -371,7 +371,7 @@ class GeneralChartComponentController: ChartThemeContainer { values.append(ChartDetailsViewModel.Value( prefix: nil, title: self.strings.revenueInUsd, - value: "≈$\(convertedValueString)", + value: "~$\(convertedValueString)", color: color, visible: firstValue.visible )) diff --git a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift index 9f1339a6e6..1450e4bae6 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift @@ -161,7 +161,7 @@ class BarsComponentController: GeneralChartComponentController { } else { text = String(format: "%0.3f", convertedValue) } - updatedLabels.append(LinesChartLabel(value: label.value, text: "≈$\(text)")) + updatedLabels.append(LinesChartLabel(value: label.value, text: "~$\(text)")) } labels = updatedLabels } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 2ffe94c99b..13fc11eb8d 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -494,7 +494,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: var label: String = "" if let subscriptionFee = state.subscriptionFee, subscriptionFee > StarsAmount.zero { let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 - label = presentationData.strings.InviteLink_Create_FeePerMonth("≈\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string + label = presentationData.strings.InviteLink_Create_FeePerMonth("~\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string } entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, StarsAmount(value: configuration.maxFee, nanos: 0))) } diff --git a/submodules/ListMessageItem/Sources/ListMessageItem.swift b/submodules/ListMessageItem/Sources/ListMessageItem.swift index a4c7ed9ba3..d017ac6395 100644 --- a/submodules/ListMessageItem/Sources/ListMessageItem.swift +++ b/submodules/ListMessageItem/Sources/ListMessageItem.swift @@ -44,7 +44,7 @@ public final class ListMessageItemInteraction { }) } -public final class ListMessageItem: ListViewItem { +public final class ListMessageItem: ListViewItem, ItemListItem { let presentationData: ChatPresentationData let systemStyle: ItemListSystemStyle let context: AccountContext @@ -57,6 +57,7 @@ public final class ListMessageItem: ListViewItem { let isGlobalSearchResult: Bool let isDownloadList: Bool let isSavedMusic: Bool + let isStoryMusic: Bool let displayFileInfo: Bool let displayBackground: Bool let canReorder: Bool @@ -64,9 +65,11 @@ public final class ListMessageItem: ListViewItem { let header: ListViewItemHeader? + public var sectionId: ItemListSectionId + public let selectable: Bool = true - public init(presentationData: ChatPresentationData, systemStyle: ItemListSystemStyle = .legacy, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain) { + public init(presentationData: ChatPresentationData, systemStyle: ItemListSystemStyle = .legacy, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false, isStoryMusic: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0) { self.presentationData = presentationData self.systemStyle = systemStyle self.context = context @@ -86,10 +89,12 @@ public final class ListMessageItem: ListViewItem { self.isGlobalSearchResult = isGlobalSearchResult self.isDownloadList = isDownloadList self.isSavedMusic = isSavedMusic + self.isStoryMusic = isStoryMusic self.displayFileInfo = displayFileInfo self.displayBackground = displayBackground self.canReorder = canReorder self.style = style + self.sectionId = sectionId } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -120,7 +125,23 @@ public final class ListMessageItem: ListViewItem { node.setupItem(self) let nodeLayout = node.asyncLayout() - let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem)) + + var topMerged = false + if let previousItem { + if let previousItem = previousItem as? ItemListItem, previousItem.sectionId == self.sectionId && !previousItem.isAlwaysPlain { + topMerged = true + } + } + + var bottomMerged = false + if let nextItem { + if let nextItem = nextItem as? ItemListItem, nextItem.sectionId == self.sectionId && !nextItem.isAlwaysPlain { + bottomMerged = true + } + } + + + let (top, bottom, dateAtBottom) = (topMerged, bottomMerged, self.getDateAtBottom(top: previousItem, bottom: nextItem)) let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom) node.updateSelectionState(animated: false) @@ -152,9 +173,22 @@ public final class ListMessageItem: ListViewItem { let nodeLayout = nodeValue.asyncLayout() + var topMerged = false + if let previousItem { + if let previousItem = previousItem as? ItemListItem, previousItem.sectionId == self.sectionId && !previousItem.isAlwaysPlain { + topMerged = true + } + } + + var bottomMerged = false + if let nextItem { + if let nextItem = nextItem as? ItemListItem, nextItem.sectionId == self.sectionId && !nextItem.isAlwaysPlain { + bottomMerged = true + } + } + async { - let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem)) - + let (top, bottom, dateAtBottom) = (topMerged, bottomMerged, self.getDateAtBottom(top: previousItem, bottom: nextItem)) let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom) Queue.mainQueue().async { completion(layout, { _ in diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 434b96002e..9942427e0e 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -446,9 +446,9 @@ public func storyLocationPickerController( ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false) controller.requestController = { _, present in - let locationPickerController = LocationPickerController(context: context, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), source: .story, initialLocation: location, completion: { location, queryId, resultId, address, countryCode in + let locationPickerController = LocationPickerController(context: context, style: .glass, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), source: .story, initialLocation: location, completion: { location, queryId, resultId, address, countryCode in completion(location, queryId, resultId, address, countryCode) }) present(locationPickerController, locationPickerController.mediaPickerContext) diff --git a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift index e57222dfb6..dfe78fa1cc 100644 --- a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift +++ b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift @@ -19,7 +19,7 @@ public func mediaPasteboardScreen( getSourceRect: (() -> CGRect?)? = nil, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil } ) -> ViewController { - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView) controller.requestController = { _, present in presentMediaPicker(.media(subjects), false, nil, nil, { mediaPicker, mediaPickerContext in present(mediaPicker, mediaPickerContext) diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD index 768e2b7bd2..ff18627371 100644 --- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD +++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD @@ -26,6 +26,9 @@ swift_library( "//submodules/Components/ActivityIndicatorComponent", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift index 939d80ae68..999385a0d6 100644 --- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift +++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift @@ -14,6 +14,9 @@ import BundleIconComponent import AnimatedStickerComponent import ActivityIndicatorComponent import GlassBarButtonComponent +import ListSectionComponent +import ListActionItemComponent +import PlainButtonComponent private final class CreateExternalMediaStreamScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -49,41 +52,51 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent final class State: ComponentState { let context: AccountContext let peerId: EnginePeer.Id + let mode: CreateExternalMediaStreamScreen.Mode private(set) var credentials: GroupCallStreamCredentials? var isDelayingLoadingIndication: Bool = true - private var credentialsDisposable: Disposable? + private let credentialsDisposable = MetaDisposable() private let activeActionDisposable = MetaDisposable() - init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise?) { + init(context: AccountContext, peerId: EnginePeer.Id, mode: CreateExternalMediaStreamScreen.Mode, credentialsPromise: Promise?) { self.context = context self.peerId = peerId + self.mode = mode super.init() + self.getCredentials(credentialsPromise: credentialsPromise) + } + + deinit { + self.credentialsDisposable.dispose() + self.activeActionDisposable.dispose() + } + + func getCredentials(credentialsPromise: Promise? = nil, revoke: Bool = false) { let credentialsSignal: Signal if let credentialsPromise = credentialsPromise { credentialsSignal = credentialsPromise.get() } else { - credentialsSignal = context.engine.calls.getGroupCallStreamCredentials(peerId: peerId, isLiveStream: false, revokePreviousCredentials: false) + var isLiveStream = false + if case let .create(isLiveStreamValue) = self.mode { + isLiveStream = isLiveStreamValue + } + credentialsSignal = self.context.engine.calls.getGroupCallStreamCredentials(peerId: self.peerId, isLiveStream: isLiveStream, revokePreviousCredentials: revoke) |> `catch` { _ -> Signal in return .never() } } - self.credentialsDisposable = (credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in + self.credentialsDisposable.set((credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } strongSelf.credentials = result strongSelf.updated(transition: .immediate) - }) - } - - deinit { - self.credentialsDisposable?.dispose() - self.activeActionDisposable.dispose() + })) } func copyCredentials(_ key: KeyPath) { @@ -160,7 +173,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent } func makeState() -> State { - return State(context: self.context, peerId: self.peerId, credentialsPromise: self.credentialsPromise) + return State(context: self.context, peerId: self.peerId, mode: self.mode, credentialsPromise: self.credentialsPromise) } static var body: Body { @@ -176,26 +189,25 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let activityIndicator = Child(ActivityIndicatorComponent.self) - let credentialsBackground = Child(RoundedRectangle.self) + let credentialsSection = Child(ListSectionComponent.self) - let credentialsStripe = Child(Rectangle.self) - let credentialsURLTitle = Child(MultilineTextComponent.self) - let credentialsURLText = Child(MultilineTextComponent.self) - - let credentialsKeyTitle = Child(MultilineTextComponent.self) - let credentialsKeyText = Child(MultilineTextComponent.self) - - let credentialsCopyURLButton = Child(Button.self) - let credentialsCopyKeyButton = Child(Button.self) +// let credentialsBackground = Child(RoundedRectangle.self) +// let credentialsStripe = Child(Rectangle.self) +// let credentialsURLTitle = Child(MultilineTextComponent.self) +// let credentialsURLText = Child(MultilineTextComponent.self) +// +// let credentialsKeyTitle = Child(MultilineTextComponent.self) +// let credentialsKeyText = Child(MultilineTextComponent.self) +// +// let credentialsCopyURLButton = Child(Button.self) +// let credentialsCopyKeyButton = Child(Button.self) return { context in let topInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0 let buttonSideInset: CGFloat = 36.0 - let credentialsSideInset: CGFloat = 16.0 - let credentialsTopInset: CGFloat = 12.0 - let credentialsTitleSpacing: CGFloat = 5.0 + let component = context.component let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let state = context.state @@ -355,124 +367,145 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent ) let textFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - text.size.width) / 2.0), y: animationFrame.maxY + 18.0), size: text.size) - context.add(text .position(CGPoint(x: textFrame.midX, y: textFrame.midY)) ) - let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize) - if let credentials = context.state.credentials { - let credentialsURLTitle = credentialsURLTitle.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .left)), - horizontalAlignment: .left, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height), - transition: context.transition + var credentialsSectionItems: [AnyComponentWithIdentity] = [] + credentialsSectionItems.append( + AnyComponentWithIdentity(id: "url", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreateExternalStream_ServerUrl, + font: Font.regular(15.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), + horizontalAlignment: .left, + truncationType: .middle, + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 5.0)), + contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "copy", + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), + action: { [weak state] in + guard let state = state else { + return + } + state.copyCredentials(\.url) + }, + animateScale: false + ) + ) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) ) - let credentialsKeyTitle = credentialsKeyTitle.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .left)), - horizontalAlignment: .left, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height), - transition: context.transition + credentialsSectionItems.append( + AnyComponentWithIdentity(id: "key", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreateExternalStream_StreamKey, + font: Font.regular(15.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), + horizontalAlignment: .left, + truncationType: .middle, + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 5.0)), + contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "copy", + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), + action: { [weak state] in + guard let state = state else { + return + } + state.copyCredentials(\.streamKey) + }, + animateScale: false + ) + ) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) ) - let credentialsURLText = credentialsURLText.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), - horizontalAlignment: .left, - truncationType: .middle, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 22.0, height: credentialsAreaSize.height), - transition: context.transition - ) - - let credentialsKeyText = credentialsKeyText.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), - horizontalAlignment: .left, - truncationType: .middle, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 48.0, height: credentialsAreaSize.height), - transition: context.transition - ) - - let credentialsBackground = credentialsBackground.update( - component: RoundedRectangle(color: theme.list.itemBlocksBackgroundColor, cornerRadius: 26.0), - availableSize: credentialsAreaSize, - transition: context.transition - ) - - let credentialsStripe = credentialsStripe.update( - component: Rectangle(color: theme.list.itemPlainSeparatorColor), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: UIScreenPixel), - transition: context.transition - ) - - let credentialsCopyURLButton = credentialsCopyURLButton.update( - component: Button( - content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), - action: { [weak state] in - guard let state = state else { - return + credentialsSectionItems.append( + AnyComponentWithIdentity(id: "revoke", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_RevokeStreamKey, font: Font.regular(17.0), textColor: theme.list.itemDestructiveColor)), + horizontalAlignment: .center, + truncationType: .middle, + maximumNumberOfLines: 1 + )), + titleAlignment: .center, + action: { [weak state] _ in + guard let state = state else { + return + } + let alertController = textAlertController(context: component.context, title: nil, text: environment.strings.CreateExternalStream_Revoke_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: environment.strings.CreateExternalStream_Revoke_Revoke, action: { [weak state] in + state?.getCredentials(revoke: true) + })]) + environment.controller()?.present(alertController, in: .window(.root)) } - state.copyCredentials(\.url) - } - ).minSize(CGSize(width: 44.0, height: 44.0)), - availableSize: CGSize(width: 44.0, height: 44.0), + ) + )) + ) + + let credentialsSection = credentialsSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: nil, + items: credentialsSectionItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: context.transition ) - - let credentialsCopyKeyButton = credentialsCopyKeyButton.update( - component: Button( - content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), - action: { [weak state] in - guard let state = state else { - return - } - state.copyCredentials(\.streamKey) - } - ).minSize(CGSize(width: 44.0, height: 44.0)), - availableSize: CGSize(width: 44.0, height: 44.0), - transition: context.transition - ) - - context.add(credentialsBackground - .position(CGPoint(x: credentialsFrame.midX, y: credentialsFrame.midY)) - ) - - context.add(credentialsStripe - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsStripe.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight)) - ) - - context.add(credentialsURLTitle - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsURLTitle.size.height / 2.0)) - ) - context.add(credentialsURLText - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLText.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsTitleSpacing + credentialsURLTitle.size.height + credentialsURLText.size.height / 2.0)) - ) - context.add(credentialsCopyURLButton - .position(CGPoint(x: credentialsFrame.maxX - 4.0 - credentialsCopyURLButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight / 2.0)) - ) - - context.add(credentialsKeyTitle - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsKeyTitle.size.height / 2.0)) - ) - context.add(credentialsKeyText - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyText.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsTitleSpacing + credentialsKeyTitle.size.height + credentialsKeyText.size.height / 2.0)) - ) - context.add(credentialsCopyKeyButton - .position(CGPoint(x: credentialsFrame.maxX - 4.0 - credentialsCopyKeyButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsItemHeight / 2.0)) - ) + context.add(credentialsSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: textFrame.maxY + 30.0 + credentialsSection.size.height / 2.0))) } else if !context.state.isDelayingLoadingIndication { + let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize) let activityIndicator = activityIndicator.update( component: ActivityIndicatorComponent(color: theme.list.controlSecondaryColor), availableSize: CGSize(width: 100.0, height: 100.0), diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index df917543dc..5e130739d4 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -732,7 +732,7 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen var price: String = "" let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 - price = "≈\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" + price = "~\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice)) entries.append(.messagePrice(presentationData.theme, sendPaidMessageStars, configuration.paidMessageMaxAmount, price)) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift index c99013d06d..52ec1c3c80 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift @@ -194,7 +194,7 @@ private func incomingMessagePrivacyScreenEntries(presentationData: PresentationD let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 - let price = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" + let price = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" entries.append(.price(value: amount.value, maxValue: configuration.paidMessageMaxAmount, price: price, isEnabled: isPremium)) entries.append(.priceInfo(commission: configuration.paidMessageCommissionPermille / 10, value: price)) diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index 3c9d2c4f20..98441dc464 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -180,10 +180,10 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { case .ton: let cryptoValue = formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat) amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) - value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" + value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" case .stars: amountString = NSAttributedString(string: presentationStringsFormattedNumber(stats.balances.availableBalance.amount, item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) - value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" + value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" isStars = true } } else { diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index 4a707b0696..a5b9d6d2d2 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -443,7 +443,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–", + item.publicShares.flatMap { "~\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–", item.presentationData.strings.Stats_Message_PrivateShares, nil, .generic @@ -485,7 +485,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "–", + item.publicShares.flatMap { "~\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "–", item.presentationData.strings.Stats_Message_PrivateShares, nil, .generic @@ -512,7 +512,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - "≈\(Int(stats.premiumAudience?.value ?? 0))", + "~\(Int(stats.premiumAudience?.value ?? 0))", item.isGroup ? item.presentationData.strings.Stats_Boosts_PremiumMembers : item.presentationData.strings.Stats_Boosts_PremiumSubscribers, (String(format: "%.02f%%", premiumSubscribers * 100.0), .generic), .generic @@ -770,7 +770,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Available, - (stats.balances.availableBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.availableBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -780,7 +780,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.currentBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Current, - (stats.balances.currentBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.currentBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -790,7 +790,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.overallRevenue.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Total, - (stats.balances.overallRevenue.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.overallRevenue.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -800,7 +800,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(additionalStats.balances.availableBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat), " ", - (additionalStats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(additionalStats.balances.availableBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (additionalStats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.availableBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -810,7 +810,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(additionalStats.balances.currentBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat), " ", - (additionalStats.balances.currentBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(additionalStats.balances.currentBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (additionalStats.balances.currentBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.currentBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -820,7 +820,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(additionalStats.balances.overallRevenue.amount, dateTimeFormat: item.presentationData.dateTimeFormat), " ", - (additionalStats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(additionalStats.balances.overallRevenue.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (additionalStats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.overallRevenue.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -834,7 +834,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Available, - (stats.balances.availableBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.availableBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -844,7 +844,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.currentBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Current, - (stats.balances.currentBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.currentBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -854,7 +854,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.overallRevenue.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Total, - (stats.balances.overallRevenue.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.overallRevenue.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -869,7 +869,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(stats.balances.availableBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Available, - (stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -879,7 +879,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(stats.balances.currentBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Current, - (stats.balances.currentBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.currentBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -889,7 +889,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(stats.balances.overallRevenue.amount, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Total, - (stats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift index 5239cae769..37333a5dc7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift @@ -25,10 +25,9 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable { public let entities: [MessageTextEntity] public let toLang: String - public let additional:[Additional] + public let additional: [Additional] public let pollSolution: Additional? - public var associatedPeerIds: [PeerId] { return [] } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 2bad40a011..9133c0a5ac 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -593,7 +593,7 @@ public extension TelegramEngine { } public func translate(texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> { - return _internal_translate_texts(network: self.account.network, texts: texts, toLang: toLang) + return _internal_translateTexts(network: self.account.network, texts: texts, toLang: toLang) } public func translateMessages(messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift index aaaaed5e05..28574d329d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift @@ -48,7 +48,7 @@ func _internal_translate(network: Network, text: String, toLang: String, entitie } } -func _internal_translate_texts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> { +func _internal_translateTexts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> { var flags: Int32 = 0 flags |= (1 << 1) @@ -112,9 +112,9 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin return .never() } - let polls = messages.compactMap { msg in - if let poll = msg.media.first as? TelegramMediaPoll { - return (poll, msg.id) + let polls = messages.compactMap { message in + if let poll = message.media.first as? TelegramMediaPoll { + return (poll, message.id) } else { return nil } @@ -128,9 +128,19 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin if let solution = poll.results.solution { texts.append((solution.text, solution.entities)) } - return _internal_translate_texts(network: account.network, texts: texts, toLang: toLang) + return _internal_translateTexts(network: account.network, texts: texts, toLang: toLang) } + let audioTranscriptions = messages.compactMap { message in + if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending { + return (audioTranscription.text, message.id) + } else { + return nil + } + } + let audioTranscriptionsSignals = audioTranscriptions.map { (text, id) in + return _internal_translate(network: account.network, text: text, toLang: toLang) + } var flags: Int32 = 0 flags |= (1 << 0) @@ -197,8 +207,8 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin } } - return combineLatest(msgs, combineLatest(pollSignals)) - |> mapToSignal { (result, pollResults) -> Signal in + return combineLatest(msgs, combineLatest(pollSignals), combineLatest(audioTranscriptionsSignals)) + |> mapToSignal { (result, pollResults, audioTranscriptionsResults) -> Signal in return account.postbox.transaction { transaction in if case let .translateResult(results) = result { var index = 0 @@ -218,6 +228,7 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin index += 1 } } + if !pollResults.isEmpty { for (i, poll) in polls.enumerated() { let result = pollResults[i] @@ -231,12 +242,12 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin if translated.0.isEmpty { translated = (poll.0.options[i].text, poll.0.options[i].entities) } - attrOptions.append(.init(text: translated.0, entities: translated.1)) + attrOptions.append(TranslationMessageAttribute.Additional(text: translated.0, entities: translated.1)) } let solution: TranslationMessageAttribute.Additional? if result.count > 1 + poll.0.options.count, !result[result.count - 1].0.isEmpty { - solution = .init(text: result[result.count - 1].0, entities: result[result.count - 1].1) + solution = TranslationMessageAttribute.Additional(text: result[result.count - 1].0, entities: result[result.count - 1].1) } else { solution = nil } @@ -251,6 +262,22 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin } } } + + if !audioTranscriptionsResults.isEmpty { + for (i, audioTranscription) in audioTranscriptions.enumerated() { + if let result = audioTranscriptionsResults[i] { + transaction.updateMessage(audioTranscription.1, update: { currentMessage in + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) } + + let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: result.0, entities: result.1, additional: [], pollSolution: nil, toLang: toLang) + attributes.append(updatedAttribute) + + return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + } + } } |> castError(TranslationError.self) } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 8c972d6962..ed139e7eb1 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1262,7 +1262,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, _): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, fromOffer): if case let .unique(gift) = gift { if !forAdditionalServiceMessage && !"".isEmpty { attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor) @@ -1290,7 +1290,33 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } } else { - if message.id.peerId == accountPeerId && assigned { + if fromOffer, let resaleStars { + let starsString: String + switch resaleStars.currency { + case .stars: + starsString = strings.Notification_StarsGiftOffer_Accepted_Stars(Int32(clamping: resaleStars.amount.value)) + case .ton: + starsString = formatTonAmountText(resaleStars.amount.value, dateTimeFormat: dateTimeFormat) + " TON" + } + let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" + var peerName = "" + if let name = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle { + peerName = name + } + if message.author?.id == accountPeerId { + let peerIds: [(Int, EnginePeer.Id?)] = [(1, message.id.peerId)] + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[0] = boldAttributes + attributes[2] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_AcceptedYou(giftTitle, peerName, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)] + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Accepted(peerName, giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } else if message.id.peerId == accountPeerId && assigned { let attributes: [Int: MarkdownAttributeSet] = [0: boldAttributes] let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Assigned(giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes) @@ -1616,10 +1642,103 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } - case .starGiftPurchaseOffer: - attributedString = nil - case .starGiftPurchaseOfferDeclined: - attributedString = nil + case let .starGiftPurchaseOffer(gift, amount, _, _, _): + let peerName = message.peers[message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + + let giftTitle: String + if case let .unique(gift) = gift { + giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))" + } else { + giftTitle = "" + } + + let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.id.peerId)] + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + + if message.author?.id == accountPeerId { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_OfferYou_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_OfferYou(peerName, priceString, giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_Offer_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Offer(peerName, priceString, giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + case let .starGiftPurchaseOfferDeclined(gift, amount, hasExpired): + let peerName = message.peers[message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.id.peerId)] + + let giftTitle: String + if case let .unique(gift) = gift { + giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))" + } else { + giftTitle = "" + } + + if hasExpired { + if message.author?.id == accountPeerId { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_ExpiredYou_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_ExpiredYou(peerName, giftTitle, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_Expired_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + let timeString = "[TODO]" + + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + attributes[3] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Expired(peerName, giftTitle, timeString, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } else { + if message.author?.id == accountPeerId { + let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_RejectedYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_Rejected_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Rejected(peerName, giftTitle, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 83a0f17408..cb1458551a 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -402,6 +402,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode", "//submodules/TelegramUI/Components/Chat/ChatRecentActionsController", diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift index 07c787129a..e3af2cbb87 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift @@ -18,23 +18,29 @@ import ComponentFlow import GlassBarButtonComponent import BundleIconComponent import EdgeEffect +import SaveToCameraRoll private final class AttachmentFileControllerArguments { let context: AccountContext + let isAudio: Bool let openGallery: () -> Void let openFiles: () -> Void + let expandSavedMusic: () -> Void let send: (Message) -> Void - init(context: AccountContext, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, send: @escaping (Message) -> Void) { + init(context: AccountContext, isAudio: Bool, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, expandSavedMusic: @escaping () -> Void, send: @escaping (Message) -> Void) { self.context = context + self.isAudio = isAudio self.openGallery = openGallery self.openFiles = openFiles + self.expandSavedMusic = expandSavedMusic self.send = send } } private enum AttachmentFileSection: Int32 { case select + case savedMusic case recent } @@ -55,6 +61,10 @@ private enum AttachmentFileEntry: ItemListNodeEntry { case selectFromGallery(PresentationTheme, String) case selectFromFiles(PresentationTheme, String) + case savedHeader(PresentationTheme, String) + case savedFile(Int32, PresentationTheme, Message?) + case showMore(PresentationTheme, String) + case recentHeader(PresentationTheme, String) case file(Int32, PresentationTheme, Message?) @@ -62,6 +72,8 @@ private enum AttachmentFileEntry: ItemListNodeEntry { switch self { case .selectFromGallery, .selectFromFiles: return AttachmentFileSection.select.rawValue + case .savedHeader, .savedFile, .showMore: + return AttachmentFileSection.savedMusic.rawValue case .recentHeader, .file: return AttachmentFileSection.recent.rawValue } @@ -73,10 +85,16 @@ private enum AttachmentFileEntry: ItemListNodeEntry { return 0 case .selectFromFiles: return 1 - case .recentHeader: + case .savedHeader: return 2 - case let .file(index, _, _): + case let .savedFile(index, _, _): return 3 + index + case .showMore: + return 9999 + case .recentHeader: + return 10000 + case let .file(index, _, _): + return 10001 + index } } @@ -94,6 +112,24 @@ private enum AttachmentFileEntry: ItemListNodeEntry { } else { return false } + case let .savedHeader(lhsTheme, lhsText): + if case let .savedHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .savedFile(lhsIndex, lhsTheme, lhsMessage): + if case let .savedFile(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) { + return true + } else { + return false + } + case let .showMore(lhsTheme, lhsText): + if case let .showMore(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .recentHeader(lhsTheme, lhsText): if case let .recentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -124,6 +160,22 @@ private enum AttachmentFileEntry: ItemListNodeEntry { return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.cloudIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: { arguments.openFiles() }) + + case let .savedHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .savedFile(_, _, message): + let interaction = ListMessageItemInteraction(openMessage: { message, _ in + arguments.send(message) + return false + }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) + + let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat + let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false)) + return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: arguments.context.account.peerId), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) + case let .showMore(theme, text): + return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: text, sectionId: self.section, editing: false, action: { + arguments.expandSavedMusic() + }) case let .recentHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .file(_, _, message): @@ -135,22 +187,53 @@ private enum AttachmentFileEntry: ItemListNodeEntry { let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false)) - return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, displayFileInfo: false, displayBackground: true, style: .blocks) + return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) } } } -private func attachmentFileControllerEntries(presentationData: PresentationData, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] { +private func attachmentFileControllerEntries(presentationData: PresentationData, mode: AttachmentFileControllerMode, state: AttachmentFileControllerState, savedMusic: [Message]?, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] { guard !empty else { return [] } var entries: [AttachmentFileEntry] = [] - entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery)) + if case .recent = mode { + entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery)) + } entries.append(.selectFromFiles(presentationData.theme, presentationData.strings.Attachment_SelectFromFiles)) + let listTitle: String + switch mode { + case .recent: + listTitle = presentationData.strings.Attachment_RecentlySentFiles + case .audio: + //TODO:localize + listTitle = "SHARED AUDIO" + } + + if case .audio = mode { + if let savedMusic, savedMusic.count > 0 { + entries.append(.savedHeader(presentationData.theme, "SAVED MUSIC".uppercased())) + var savedMusic = savedMusic + var showMore = false + if savedMusic.count > 4 && !state.savedMusicExpanded { + savedMusic = Array(savedMusic.prefix(3)) + showMore = true + } + var i: Int32 = 0 + for file in savedMusic { + entries.append(.savedFile(i, presentationData.theme, file)) + i += 1 + } + if showMore { + entries.append(.showMore(presentationData.theme, "Show More")) + } + } + } + if let recentDocuments = recentDocuments { if recentDocuments.count > 0 { - entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased())) + entries.append(.recentHeader(presentationData.theme, listTitle.uppercased())) var i: Int32 = 0 for file in recentDocuments { entries.append(.file(i, presentationData.theme, file)) @@ -158,7 +241,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, } } } else { - entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased())) + entries.append(.recentHeader(presentationData.theme, listTitle.uppercased())) for i in 0 ..< 11 { entries.append(.file(Int32(i), presentationData.theme, nil)) } @@ -185,6 +268,8 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon var delayDisappear = false + var hasBottomEdgeEffect = true + var resetForReuseImpl: () -> Void = {} public func resetForReuse() { self.resetForReuseImpl() @@ -229,31 +314,41 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon transition.updateFrame(view: topEdgeEffectView, frame: topEdgeEffectFrame) topEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) - let bottomEdgeEffectView: EdgeEffectView - if let current = self.bottomEdgeEffectView { - bottomEdgeEffectView = current - } else { - bottomEdgeEffectView = EdgeEffectView() - self.view.addSubview(bottomEdgeEffectView) - self.bottomEdgeEffectView = bottomEdgeEffectView + if self.hasBottomEdgeEffect { + let bottomEdgeEffectView: EdgeEffectView + if let current = self.bottomEdgeEffectView { + bottomEdgeEffectView = current + } else { + bottomEdgeEffectView = EdgeEffectView() + self.view.addSubview(bottomEdgeEffectView) + self.bottomEdgeEffectView = bottomEdgeEffectView + } + + let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight)) + transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame) + transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0) + bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) + } else if let bottomEdgeEffectView = self.bottomEdgeEffectView { + bottomEdgeEffectView.removeFromSuperview() } - - let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight)) - transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame) - transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0) - bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) } } private struct AttachmentFileControllerState: Equatable { var searching: Bool + var savedMusicExpanded: Bool } -public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController { +public enum AttachmentFileControllerMode { + case recent + case audio +} + +public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: AttachmentFileControllerMode = .recent, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController { let actionsDisposable = DisposableSet() - let statePromise = ValuePromise(AttachmentFileControllerState(searching: false), ignoreRepeated: true) - let stateValue = Atomic(value: AttachmentFileControllerState(searching: false)) + let statePromise = ValuePromise(AttachmentFileControllerState(searching: false, savedMusicExpanded: false), ignoreRepeated: true) + let stateValue = Atomic(value: AttachmentFileControllerState(searching: false, savedMusicExpanded: false)) let updateState: ((AttachmentFileControllerState) -> AttachmentFileControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -265,39 +360,87 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre var updateIsSearchingImpl: ((Bool) -> Void)? let arguments = AttachmentFileControllerArguments( context: context, + isAudio: mode == .audio, openGallery: { presentGallery() }, openFiles: { presentFiles() }, + expandSavedMusic: { + updateState { state in + var updatedState = state + updatedState.savedMusicExpanded = true + return updatedState + } + }, send: { message in - let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true)) - |> `catch` { _ in - return .single(.result([])) - } - |> mapToSignal { result -> Signal<[Message], NoError> in - guard case let .result(result) = result else { - return .complete() + if message.id.namespace == Namespaces.Message.Local { + if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { + send(.standalone(media: file)) } - return .single(result) - } - |> deliverOnMainQueue).startStandalone(next: { messages in - if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { - send(.message(message: MessageReference(message), media: file)) + } else { + let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true)) + |> `catch` { _ in + return .single(.result([])) } - dismissImpl?() - }) + |> mapToSignal { result -> Signal<[Message], NoError> in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).startStandalone(next: { messages in + if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { + send(.message(message: MessageReference(message), media: file)) + } + dismissImpl?() + }) + } } ) - let recentDocuments: Signal<[Message]?, NoError> = .single(nil) - |> then( - context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil) - |> map { result -> [Message]? in - return result.0.messages - } - ) + let recentDocuments: Signal<[Message]?, NoError> + let savedMusicContext: ProfileSavedMusicContext? + let savedMusic: Signal<[Message]?, NoError> + switch mode { + case .recent: + recentDocuments = .single(nil) + |> then( + context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil) + |> map { result -> [Message]? in + return result.0.messages + } + ) + savedMusicContext = nil + savedMusic = .single(nil) + case .audio: + recentDocuments = .single(nil) + |> then( + context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: "", state: nil) + |> map { result -> [Message]? in + return result.0.messages + } + ) + savedMusicContext = ProfileSavedMusicContext(account: context.account, peerId: context.account.peerId) + savedMusic = .single(nil) + |> then( + savedMusicContext!.state + |> map { state in + let peerId = context.account.peerId + var messages: [Message] = [] + let peers = SimpleDictionary() +// if let peer { +// peers[peerId] = peer._asPeer() +// } + for file in state.files { + let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max)) + messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])) + } + return messages + } + ) + } let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData @@ -306,14 +449,12 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre let previousRecentDocuments = Atomic<[Message]?>(value: nil) let signal = combineLatest(queue: Queue.mainQueue(), - presentationData, - recentDocuments, - statePromise.get() - ) - |> map { presentationData, recentDocuments, - state -> (ItemListControllerState, (ItemListNodeState, Any)) in + savedMusic, + statePromise.get() + ) + |> map { presentationData, recentDocuments, savedMusic, state -> (ItemListControllerState, (ItemListNodeState, Any)) in var presentationData = presentationData let updatedTheme = presentationData.theme.withModalBlocksBackground() @@ -397,9 +538,17 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre rightNavigationButton = searchButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) } } + let title: String + switch mode { + case .recent: + title = presentationData.strings.Attachment_File + case .audio: + title = "Audio" + } + let controllerState = ItemListControllerState( presentationData: ItemListPresentationData(presentationData), - title: .text(presentationData.strings.Attachment_File), + title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), @@ -424,7 +573,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre var searchItem: ItemListControllerSearch? if state.searching { - searchItem = AttachmentFileSearchItem(context: context, presentationData: presentationData, focus: { + searchItem = AttachmentFileSearchItem(context: context, mode: mode, presentationData: presentationData, focus: { expandImpl?() }, cancel: { updateState { state in @@ -441,14 +590,18 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre }) } - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, mode: mode, state: state, savedMusic: savedMusic, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() + let _ = savedMusicContext?.state } let controller = AttachmentFileControllerImpl(context: context, state: signal, hideNavigationBarBackground: true) + if case .audio = mode { + controller.hasBottomEdgeEffect = false + } controller.delayDisappear = true controller.visibleBottomContentOffsetChanged = { [weak controller] offset in switch offset { @@ -490,3 +643,34 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre } return controller } + +public func storyAudioPickerController( + context: AccountContext, + selectFromFiles: @escaping () -> Void, + dismissed: @escaping () -> Void, + completion: @escaping (AnyMediaReference) -> Void, +) -> ViewController { + var dismissImpl: (() -> Void)? + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) + let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false) + controller.requestController = { _, present in + let filePickerController = makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: .audio, bannedSendMedia: nil, presentGallery: {}, presentFiles: { + selectFromFiles() + dismissImpl?() + }, send: { file in + completion(file) + dismissImpl?() + }) as! AttachmentFileControllerImpl + present(filePickerController, filePickerController.mediaPickerContext) + } + controller.navigationPresentation = .flatModal + controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + controller.didDismiss = { + dismissed() + } + dismissImpl = { [weak controller] in + controller?.dismiss(animated: true) + } + return controller +} diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift index 3eaed92670..ed7b71bd06 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift @@ -21,6 +21,7 @@ import SearchInputPanelComponent final class AttachmentFileSearchItem: ItemListControllerSearch { let context: AccountContext + let mode: AttachmentFileControllerMode let presentationData: PresentationData let focus: () -> Void let cancel: () -> Void @@ -31,8 +32,9 @@ final class AttachmentFileSearchItem: ItemListControllerSearch { private var activity: ValuePromise = ValuePromise(ignoreRepeated: false) private let activityDisposable = MetaDisposable() - init(context: AccountContext, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) { + init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) { self.context = context + self.mode = mode self.presentationData = presentationData self.focus = focus self.cancel = cancel @@ -69,7 +71,7 @@ final class AttachmentFileSearchItem: ItemListControllerSearch { } func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode { - return AttachmentFileSearchItemNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in + return AttachmentFileSearchItemNode(context: self.context, mode: self.mode, presentationData: self.presentationData, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in self?.activity.set(value) }, dismissInput: self.dismissInput) } @@ -77,8 +79,8 @@ final class AttachmentFileSearchItem: ItemListControllerSearch { private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode { private let context: AccountContext - private let theme: PresentationTheme - private let strings: PresentationStrings + private let mode: AttachmentFileControllerMode + private let presentationData: PresentationData private let focus: () -> Void private let cancel: () -> Void @@ -88,14 +90,14 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode { private var validLayout: ContainerViewLayout? - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) { + init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) { self.context = context - self.theme = theme - self.strings = strings + self.mode = mode + self.presentationData = presentationData self.focus = focus self.cancel = cancel - self.containerNode = AttachmentFileSearchContainerNode(context: context, forceTheme: nil, send: { message in + self.containerNode = AttachmentFileSearchContainerNode(context: context, mode: mode, presentationData: presentationData, send: { message in send(message) }, updateActivity: updateActivity) @@ -136,15 +138,16 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode { transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition) + //TODO:localize let searchInputSize = self.searchInput.update( transition: .immediate, component: AnyComponent( SearchInputPanelComponent( - theme: self.theme, - strings: self.strings, + theme: self.presentationData.theme, + strings: self.presentationData.strings, metrics: layout.metrics, safeInsets: layout.safeInsets, - placeholder: self.strings.Attachment_FilesSearchPlaceholder, + placeholder: self.mode == .audio ? "Search shared audio" : self.presentationData.strings.Attachment_FilesSearchPlaceholder, updated: { [weak self] query in guard let self else { return @@ -251,7 +254,7 @@ private final class AttachmentFileSearchEntry: Comparable, Identifiable { interaction.send(message) return false }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) - return ListMessageItem(presentationData: ChatPresentationData(presentationData: interaction.context.sharedContext.currentPresentationData.with({$0})), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain) + return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain) } } @@ -293,10 +296,8 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon private let emptyQueryDisposable = MetaDisposable() private let searchDisposable = MetaDisposable() - private let forceTheme: PresentationTheme? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let presentationDataPromise: Promise private var _hasDim: Bool = false @@ -304,17 +305,12 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon return _hasDim } - public init(context: AccountContext, forceTheme: PresentationTheme?, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) { + public init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) { self.context = context self.send = send - let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData - self.forceTheme = forceTheme - if let forceTheme = self.forceTheme { - self.presentationData = self.presentationData.withUpdated(theme: forceTheme) - } self.presentationDataPromise = Promise(self.presentationData) self.dimNode = ASDisplayNode() @@ -380,13 +376,26 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon return .single(nil) } - let signal: Signal<[Message]?, NoError> = .single(nil) - |> then( - context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil) - |> map { result -> [Message]? in - return result.0.messages - } - ) + let signal: Signal<[Message]?, NoError> + switch mode { + case .recent: + signal = .single(nil) + |> then( + context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil) + |> map { result -> [Message]? in + return result.0.messages + } + ) + case .audio: + signal = .single(nil) + |> then( + context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: query, state: nil) + |> map { result -> [Message]? in + return result.0.messages + } + ) + } + updateActivity(true) return combineLatest(signal, presentationDataPromise.get()) @@ -420,25 +429,19 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon } })) - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in - if let strongSelf = self { - var presentationData = presentationData - - let previousTheme = strongSelf.presentationData.theme - let previousStrings = strongSelf.presentationData.strings - - if let forceTheme = strongSelf.forceTheme { - presentationData = presentationData.withUpdated(theme: forceTheme) - } - - strongSelf.presentationData = presentationData - - if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { - strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) - } - } - }) +// self.presentationDataDisposable = (context.sharedContext.presentationData +// |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in +// if let strongSelf = self { +// let previousTheme = strongSelf.presentationData.theme +// let previousStrings = strongSelf.presentationData.strings +// +// strongSelf.presentationData = presentationData +// +// if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { +// strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) +// } +// } +// }) self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index 7e0d886f68..cfc119906d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -80,6 +80,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index bcbf47663a..f15cdb59df 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -70,6 +70,7 @@ import ChatMessageStoryMentionContentNode import ChatMessageUnsupportedBubbleContentNode import ChatMessageWallpaperBubbleContentNode import ChatMessageGiftBubbleContentNode +import ChatMessageGiftOfferBubbleContentNode import ChatMessageGiveawayBubbleContentNode import ChatMessageJoinedChannelBubbleContentNode import ChatMessageFactCheckBubbleContentNode @@ -246,6 +247,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .suggestedBirthday = action.action { result.append((message, ChatMessageBirthdateSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } else if case .starGiftPurchaseOffer = action.action { + result.append((message, ChatMessageGiftOfferBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else { if !canAddMessageReactions(message: message) { needReactions = false @@ -2821,6 +2824,48 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI actionButtonsFinalize = buttonsLayout lastNodeTopPosition = .None(.Both) + } else if incoming, let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(_, _, expireDate, isAccepted, isDeclined) = action.action, !isAccepted && !isDeclined { + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + if expireDate <= currentTimestamp { + + } else { + var buttonDeclineValue: UInt8 = 0 + let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1)) + var buttonApproveValue: UInt8 = 1 + let buttonApprove = MemoryBuffer(data: Data(bytes: &buttonApproveValue, count: 1)) + + let customInfos: [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo] = [ + buttonDecline: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: true, + icon: .suggestedPostReject + ), + buttonApprove: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: true, + icon: .suggestedPostApprove + ) + ] + //TODO:localize + let (minWidth, buttonsLayout) = actionButtonsLayout( + item.context, + item.presentationData.theme, + item.presentationData.chatBubbleCorners, + item.presentationData.strings, + item.controllerInteraction.presentationContext.backgroundNode, + ReplyMarkupMessageAttribute( + rows: [ + ReplyMarkupRow(buttons: [ + ReplyMarkupButton(title: "Reject", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)), + ReplyMarkupButton(title: "Accept", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove)) + ]) + ], + flags: [], + placeholder: nil + ), customInfos, item.message, baseWidth) + maxContentWidth = max(maxContentWidth, minWidth) + actionButtonsFinalize = buttonsLayout + + lastNodeTopPosition = .None(.Both) + } } else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil { var canApprove = true if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), !mainChannel.hasPermission(.sendSomething) { @@ -4772,7 +4817,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { let actionButtonsNode = actionButtonsSizeAndApply.1(animation) - let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0) + var actionButtonsOriginX = backgroundFrame.minX + if case .center = alignment { + actionButtonsOriginX += 3.0 + } else { + actionButtonsOriginX += incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right + } + let actionButtonsFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0) if actionButtonsNode !== strongSelf.actionButtonsNode { strongSelf.actionButtonsNode = actionButtonsNode actionButtonsNode.buttonPressed = { [weak strongSelf] button, progress in diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index b65bd9c04a..df5f8f86aa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -430,6 +430,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View var buttonIcon: String? var ribbonTitle = "" + var customRibbonColors: [UIColor]? var textSpacing: CGFloat = 0.0 var isStarGift = false @@ -686,7 +687,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, _): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer): if case let .unique(uniqueGift) = gift { isStarGift = true @@ -715,7 +716,13 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { title = item.presentationData.strings.Notification_StarGift_Title(authorName).string } text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" : "**\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" - ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift + if fromOffer { + //TODO:localize + ribbonTitle = incoming ? "" : "sold" + customRibbonColors = [UIColor(rgb: 0xd9433a), UIColor(rgb: 0xff645b)] + } else { + ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift + } buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View modelTitle = item.presentationData.strings.Notification_StarGift_Model backdropTitle = item.presentationData.strings.Notification_StarGift_Backdrop @@ -1285,7 +1292,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if ribbonTextLayout.size.width > 0.0 { if strongSelf.ribbonBackgroundNode.image == nil { - if let uniqueBackgroundColor { + if let customRibbonColors { + strongSelf.ribbonBackgroundNode.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: customRibbonColors, direction: .mirroredDiagonal) + } else if let uniqueBackgroundColor { let colors = [ uniqueBackgroundColor.withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89), uniqueBackgroundColor.withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD new file mode 100644 index 0000000000..db4d3ca487 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD @@ -0,0 +1,36 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageGiftOfferBubbleContentNode", + module_name = "ChatMessageGiftOfferBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/LocalizedPeerData", + "//submodules/UrlEscaping", + "//submodules/TelegramStringFormatting", + "//submodules/WallpaperBackgroundNode", + "//submodules/ReactionSelectionNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/Markdown", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift new file mode 100644 index 0000000000..2372c3183d --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift @@ -0,0 +1,341 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import ComponentFlow +import TelegramCore +import AccountContext +import TelegramPresentationData +import TelegramUIPreferences +import TextFormat +import LocalizedPeerData +import UrlEscaping +import TelegramStringFormatting +import WallpaperBackgroundNode +import ReactionSelectionNode +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ChatControllerInteraction +import ShimmerEffect +import Markdown +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import TextNodeWithEntities +import GiftItemComponent + +private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { + return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true) +} + +public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode { + private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? + private let titleNode: TextNode + private let subtitleNode: TextNodeWithEntities + private let giftIcon = ComponentView() + + private var absoluteRect: (CGRect, CGSize)? + + private var isPlaying: Bool = false + + override public var disablesClipping: Bool { + return true + } + + override public var visibility: ListViewItemNodeVisibility { + didSet { + let wasVisible = oldValue != .none + let isVisible = self.visibility != .none + + if wasVisible != isVisible { + self.visibilityStatus = isVisible + + switch self.visibility { + case .none: + self.subtitleNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.subtitleNode.visibilityRect = subRect + } + } + } + } + + private var visibilityStatus: Bool? { + didSet { + if self.visibilityStatus != oldValue { + self.updateVisibility() + } + } + } + + private var fetchDisposable: Disposable? + private var setupTimestamp: Double? + + private var cachedTonImage: (UIImage, UIColor)? + + required public init() { + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false + + self.subtitleNode = TextNodeWithEntities() + self.subtitleNode.textNode.isUserInteractionEnabled = false + self.subtitleNode.textNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode.textNode) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.fetchDisposable?.dispose() + } + + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) + + return { item, layoutConstants, _, _, _, _ in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) + + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + var giftSize = CGSize(width: 260.0, height: 240.0) + var uniqueGift: StarGift.UniqueGift? + + let incoming: Bool + if item.message.id.peerId == item.context.account.peerId && item.message.forwardInfo == nil { + incoming = true + } else { + incoming = item.message.effectivelyIncoming(item.context.account.peerId) + } + + let textColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText + + let text: String + let additionalText: String + + var hasActionButtons = false + if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(gift, amount, expireDate, isAccepted, isDeclined) = action.action { + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + let priceString: String + switch amount.currency { + case .stars: + priceString = "\(amount.amount) Stars" + case .ton: + priceString = "\(amount.amount) TON" + } + + let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + let giftTitle: String + if case let .unique(gift) = gift { + giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: item.presentationData.dateTimeFormat))" + uniqueGift = gift + } else { + giftTitle = "" + } + + //TODO:localize + if incoming { + text = "**\(peerName)** offered you **\(priceString)** for your gift **\(giftTitle)**." + } else { + text = "You offered **\(peerName)** **\(priceString)** for their gift **\(giftTitle)**." + } + + if isAccepted { + additionalText = "This offer was accepted." + } else if isDeclined { + additionalText = "This offer was rejected." + } else if expireDate > currentTimestamp { + func textForTimeout(_ value: Int32) -> String { + if value < 3600 { + let minutes = value / 60 + //let seconds = value % 60 + //let secondsPadding = seconds < 10 ? "0" : "" + return "\(minutes)m" //\(secondsPadding)\(seconds)" + } else { + let hours = value / 3600 + let minutes = (value % 3600) / 60 + let minutesPadding = minutes < 10 ? "0" : "" + //let seconds = value % 60 + //let secondsPadding = seconds < 10 ? "0" : "" + return "\(hours)h \(minutesPadding)\(minutes)m" //:\(secondsPadding)\(seconds)" + } + } + let delta = expireDate - currentTimestamp + additionalText = "This offer expires in \(textForTimeout(delta))." + + if incoming { + hasActionButtons = true + } + } else { + additionalText = "This offer has expired." + } + } else { + text = "" + additionalText = "" + } + + let titleAttributedString = NSMutableAttributedString(attributedString: NSAttributedString(string: additionalText, font: Font.regular(13.0), textColor: textColor, paragraphAlignment: .center)) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + + let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + giftSize.height = titleLayout.size.height + subtitleLayout.size.height + 162.0 + + let backgroundSize = CGSize(width: giftSize.width, height: giftSize.height + 4.0) + + return (backgroundSize.width, { boundingWidth in + return (backgroundSize, { [weak self] animation, synchronousLoads, info in + if let strongSelf = self { + strongSelf.item = item + + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - giftSize.width) / 2.0), y: 0.0), size: giftSize) + let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) + + strongSelf.updateVisibility() + + let _ = titleApply() + let _ = subtitleApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.controllerInteraction.presentationContext.animationCache, + renderer: item.controllerInteraction.presentationContext.animationRenderer, + placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, + attemptSynchronous: synchronousLoads + )) + + + let textFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: mediaBackgroundFrame.minY + 126.0), size: subtitleLayout.size) + strongSelf.subtitleNode.textNode.frame = textFrame + + let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: textFrame.maxY + 23.0), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame + + if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + backgroundContent.cornerRadius = 24.0 + + strongSelf.mediaBackgroundContent = backgroundContent + strongSelf.insertSubnode(backgroundContent, at: 0) + } + + if let backgroundContent = strongSelf.mediaBackgroundContent { + animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil) + backgroundContent.clipsToBounds = true + + if hasActionButtons { + backgroundContent.cornerRadius = 0.0 + if backgroundContent.view.mask == nil { + backgroundContent.view.mask = UIImageView(image: generateImage(mediaBackgroundFrame.size, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + + context.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height * 0.5), cornerWidth: 24.0, cornerHeight: 24.0, transform: nil)) + context.addPath(CGPath(roundedRect: CGRect(x: 0, y: size.height * 0.5 - 30.0, width: size.width, height: size.height * 0.5 + 30.0), cornerWidth: 8.0, cornerHeight: 8.0, transform: nil)) + context.fillPath() + })) + } + } else { + backgroundContent.view.mask = nil + backgroundContent.cornerRadius = 24.0 + } + } + + if let uniqueGift { + let iconSize = CGSize(width: 94.0, height: 94.0) + let _ = strongSelf.giftIcon.update( + transition: .immediate, + component: AnyComponent(GiftItemComponent( + context: item.context, + theme: item.presentationData.theme.theme, + strings: item.presentationData.strings, + peer: nil, + subject: .uniqueGift(gift: uniqueGift, price: nil), + mode: .thumbnail + )), + environment: {}, + containerSize: iconSize + ) + if let giftIconView = strongSelf.giftIcon.view { + if giftIconView.superview == nil { + // backgroundView.layer.cornerRadius = 20.0 + //backgroundView.clipsToBounds = true + strongSelf.view.addSubview(giftIconView) + } + giftIconView.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 17.0), size: iconSize) + } + } + + if let (rect, size) = strongSelf.absoluteRect { + strongSelf.updateAbsoluteRect(rect, within: size) + } + + switch strongSelf.visibility { + case .none: + strongSelf.subtitleNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + strongSelf.subtitleNode.visibilityRect = subRect + } + } + }) + }) + }) + } + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteRect = (rect, containerSize) + + if let mediaBackgroundContent = self.mediaBackgroundContent { + var backgroundFrame = mediaBackgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + + override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + + } + + override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + + } + + override public func unreadMessageRangeUpdated() { + self.updateVisibility() + } + + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if self.mediaBackgroundContent?.frame.contains(point) == true { + return ChatMessageBubbleContentTapAction(content: .openMessage) + } else { + return ChatMessageBubbleContentTapAction(content: .none) + } + } + + private func updateVisibility() { + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD index cf18fe2633..555ad4a0a5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode", "//submodules/AnimatedCountLabelNode", "//submodules/AudioWaveform", "//submodules/DeviceProximity", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 60857c7020..550414b478 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -35,6 +35,7 @@ import TelegramStringFormatting import AnimatedCountLabelNode import AudioWaveform import DeviceProximity +import ShimmeringLinkNode private struct FetchControls { let fetch: (Bool) -> Void @@ -123,6 +124,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { public let fetchingTextNode: ImmediateTextNode public let fetchingCompactTextNode: ImmediateTextNode private let countNode: ImmediateAnimatedCountLabelNode + private var shimmeringNodes: [ShimmeringLinkNode] = [] public var waveformView: ComponentHostView? @@ -809,23 +811,28 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var displayTrailingAnimatedDots = false + var isTranslating = false if let transcribedText = transcribedText, case .expanded = effectiveAudioTranscriptionState { switch transcribedText { case let .success(text, isPending): textString = NSAttributedString(string: text, font: textFont, textColor: messageTheme.primaryTextColor) - /*#if DEBUG - var isPending = isPending - if "".isEmpty { - isPending = true - } - #endif*/ - if isPending { let modifiedString = NSMutableAttributedString(attributedString: textString!) modifiedString.append(NSAttributedString(string: "...", font: textFont, textColor: .clear)) displayTrailingAnimatedDots = true textString = modifiedString + } else { + if let translateToLanguage = arguments.associatedData.translateToLanguage, !text.isEmpty && arguments.incoming { + isTranslating = true + for attribute in arguments.message.attributes { + if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { + textString = NSAttributedString(string: attribute.text, font: textFont, textColor: messageTheme.primaryTextColor) + isTranslating = false + break + } + } + } } case let .error(error): let errorTextFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 15.0 / 17.0)) @@ -1538,6 +1545,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } else { strongSelf.dateAndStatusNode.pressed = nil } + + strongSelf.updateIsTranslating(isTranslating) } }) }) @@ -1545,6 +1554,39 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } } + private func updateIsTranslating(_ isTranslating: Bool) { + guard let arguments = self.arguments else { + return + } + var rects: [[CGRect]] = [] + let titleRects = (self.textNode.rangeRects(in: NSRange(location: 0, length: self.textNode.cachedLayout?.attributedString?.length ?? 0))?.rects ?? []).map { self.textNode.view.convert($0, to: self.textClippingNode.view) } + rects.append(titleRects) + + if isTranslating, !rects.isEmpty { + if self.shimmeringNodes.isEmpty { + for rects in rects { + let shimmeringNode = ShimmeringLinkNode(color: arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)) + shimmeringNode.updateRects(rects) + shimmeringNode.frame = self.bounds + shimmeringNode.updateLayout(self.bounds.size) + shimmeringNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.shimmeringNodes.append(shimmeringNode) + self.textClippingNode.insertSubnode(shimmeringNode, belowSubnode: self.textNode) + } + } + } else if !self.shimmeringNodes.isEmpty { + let shimmeringNodes = self.shimmeringNodes + self.shimmeringNodes = [] + + for shimmeringNode in shimmeringNodes { + shimmeringNode.alpha = 0.0 + shimmeringNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak shimmeringNode] _ in + shimmeringNode?.removeFromSupernode() + }) + } + } + } + private func updateStatus(animated: Bool) { guard let resourceStatus = self.resourceStatus else { return diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD index 1d1499967b..393edc5f09 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD @@ -45,6 +45,9 @@ swift_library( "//submodules/AnimatedCountLabelNode", "//submodules/HexColor", "//submodules/QrCodeUI", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/SheetComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index 6b69db21d9..8ada9fca44 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift @@ -36,6 +36,10 @@ import SegmentedControlNode import AnimatedCountLabelNode import HexColor import QrCodeUI +import ComponentFlow +import GlassBarButtonComponent +import SheetComponent +import BundleIconComponent private func closeButtonImage(theme: PresentationTheme) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in @@ -717,7 +721,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen { } private func iconColors(theme: PresentationTheme) -> [String: UIColor] { - let accentColor = theme.actionSheet.controlAccentColor + let accentColor = theme.rootController.navigationBar.glassBarButtonForegroundColor var colors: [String: UIColor] = [:] colors["Sunny.Path 14.Path.Stroke 1"] = accentColor colors["Sunny.Path 15.Path.Stroke 1"] = accentColor @@ -745,18 +749,6 @@ private func interpolateColors(from: [String: UIColor], to: [String: UIColor], f private let defaultEmoticon = "🏠" -private func generateShadowImage() -> UIImage? { - return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setShadow(offset: CGSize(width: 0.0, height: -0.5), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.4).cgColor) - context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor) - let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0)), cornerRadius: 16.0) - context.addPath(path.cgPath) - context.fillPath() - })?.stretchableImage(withLeftCapWidth: 20, topCapHeight: 0) -} - private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData @@ -768,14 +760,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg private let scrollNodeContentNode: ASDisplayNode private let contentContainerNode: ASDisplayNode private let topContentContainerNode: SparseNode - private let shadowNode: ASImageNode - private let effectNode: ASDisplayNode private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode + private let contentBackgroundView: SheetBackgroundView + private let cancelButton = ComponentView() + private let switchThemeButton = ComponentView() private let titleNode: ASTextNode private let segmentedNode: SegmentedControlNode - private let cancelButton: HighlightableButtonNode - private let switchThemeButton: HighlightTrackingButtonNode + private let cancelButtonNode: HighlightableButtonNode + private let switchThemeButtonNode: HighlightTrackingButtonNode private let animationContainerNode: ASDisplayNode private var animationNode: AnimationNode private let doneButton: SolidRoundedButtonNode @@ -841,28 +833,17 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.topContentContainerNode = SparseNode() self.topContentContainerNode.isOpaque = false - - self.shadowNode = ASImageNode() - self.shadowNode.contentMode = .scaleToFill - self.shadowNode.image = generateShadowImage() self.backgroundNode = ASDisplayNode() self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 + self.backgroundNode.cornerRadius = 38.0 self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance) - let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor let textColor = self.presentationData.theme.actionSheet.primaryTextColor - let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark - - self.effectNode = ASDisplayNode(viewBlock: { - return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) - }) - - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor + + self.contentBackgroundView = SheetBackgroundView() self.titleNode = ASTextNode() let title: String @@ -878,12 +859,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.segmentedNode.isHidden = !self.contentNode.hasVideo self.titleNode.isHidden = !self.segmentedNode.isHidden - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) - self.cancelButton.accessibilityLabel = self.presentationData.strings.Common_Close - self.cancelButton.accessibilityTraits = [.button] + self.cancelButtonNode = HighlightableButtonNode() + self.cancelButtonNode.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + self.cancelButtonNode.accessibilityLabel = self.presentationData.strings.Common_Close + self.cancelButtonNode.accessibilityTraits = [.button] - self.switchThemeButton = HighlightTrackingButtonNode() + self.switchThemeButtonNode = HighlightTrackingButtonNode() self.animationContainerNode = ASDisplayNode() self.animationContainerNode.isUserInteractionEnabled = false @@ -916,13 +897,10 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.scrollNodeContentNode.addSubnode(self.contentNode) - self.scrollNodeContentNode.addSubnode(self.shadowNode) self.scrollNodeContentNode.addSubnode(self.backgroundNode) self.scrollNodeContentNode.addSubnode(self.contentContainerNode) self.scrollNodeContentNode.addSubnode(self.topContentContainerNode) - self.backgroundNode.addSubnode(self.effectNode) - self.backgroundNode.addSubnode(self.contentBackgroundNode) self.contentContainerNode.addSubnode(self.titleNode) self.contentContainerNode.addSubnode(self.segmentedNode) self.contentContainerNode.addSubnode(self.doneButton) @@ -930,12 +908,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.topContentContainerNode.addSubnode(self.animationContainerNode) self.animationContainerNode.addSubnode(self.animationNode) - self.topContentContainerNode.addSubnode(self.switchThemeButton) + //self.topContentContainerNode.addSubnode(self.switchThemeButtonNode) self.topContentContainerNode.addSubnode(self.listNode) - self.topContentContainerNode.addSubnode(self.cancelButton) + //self.topContentContainerNode.addSubnode(self.cancelButtonNode) - self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + self.switchThemeButtonNode.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) + self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) self.segmentedNode.selectedIndexChanged = { [weak self] index in guard let strongSelf = self, let contentNode = strongSelf.contentNode as? MessageContentNode, let videoNode = contentNode.videoNode else { @@ -1181,7 +1159,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg } })) - self.switchThemeButton.highligthedChanged = { [weak self] highlighted in + self.switchThemeButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.animationContainerNode.layer.removeAnimation(forKey: "opacity") @@ -1280,14 +1258,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + self.cancelButtonNode.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) self.scanButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor)) let previousIconColors = iconColors(theme: previousTheme) let newIconColors = iconColors(theme: self.presentationData.theme) - if !self.switchThemeButton.isUserInteractionEnabled { + if !self.switchThemeButtonNode.isUserInteractionEnabled { let themeCrossfadeDuration: Double = 0.3 let themeCrossfadeDelay: Double = 0.25 @@ -1312,6 +1290,8 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never } + self.backgroundNode.view.addSubview(self.contentBackgroundView) + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true } @@ -1320,9 +1300,9 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg } @objc private func switchThemePressed() { - self.switchThemeButton.isUserInteractionEnabled = false + self.switchThemeButtonNode.isUserInteractionEnabled = false Queue.mainQueue().after(0.5) { - self.switchThemeButton.isUserInteractionEnabled = true + self.switchThemeButtonNode.isUserInteractionEnabled = true } self.animateCrossfade(animateIcon: false) @@ -1372,18 +1352,11 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg }) } - Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) { - if let effectView = self.effectNode.view as? UIVisualEffectView { - UIView.animate(withDuration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) { - effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } completion: { _ in - } - } - - let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration) - } +// Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) { +// let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear +// self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor +// self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration) +// } if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.contentContainerNode.frame @@ -1403,7 +1376,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg private var animatedOut = false public func animateIn() { - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let offset = self.bounds.size.height - self.contentBackgroundView.frame.minY if let (layout, _) = self.containerLayout { self.scrollNodeContentNode.cornerRadius = layout.deviceMetrics.screenCornerRadius @@ -1424,7 +1397,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.wrappingScrollNode.view.isScrollEnabled = false - let distance = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let distance = self.bounds.size.height - self.contentBackgroundView.frame.minY if let velocity { let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity / distance) self.wrappingScrollNode.layer.animateSpring(from: 0.0 as NSNumber, to: -distance as NSNumber, keyPath: "bounds.origin.y", duration: 0.45, delay: 0.0, initialVelocity: initialVelocity, damping: 124.0, removeOnCompletion: false, additive: true, completion: { _ in @@ -1473,24 +1446,81 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) let sideInset = floor((layout.size.width - width) / 2.0) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight - 6.0), size: CGSize(width: width, height: contentHeight)).insetBy(dx: 6.0, dy: 0.0) let contentFrame = contentContainerFrame var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) if backgroundFrame.minY < contentFrame.minY { backgroundFrame.origin.y = contentFrame.minY } - - let shadowFrame = CGRect(x: backgroundFrame.minX, y: backgroundFrame.minY - 8.0, width: backgroundFrame.width, height: 40.0) - transition.updateFrame(node: self.shadowNode, frame: shadowFrame) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + self.contentBackgroundView.update(size: contentFrame.size, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, topCornerRadius: 38.0, bottomCornerRadius: layout.deviceMetrics.screenCornerRadius - 2.0, transition: .immediate) + transition.updateFrame(view: self.contentBackgroundView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + let barButtonSize = CGSize(width: 40.0, height: 40.0) + let cancelButtonSize = self.cancelButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: barButtonSize, + backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: self.presentationData.theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { [weak self] _ in + self?.cancelButtonPressed() + } + )), + environment: {}, + containerSize: barButtonSize + ) + let cancelButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelButtonSize) + if let view = self.cancelButton.view { + if view.superview == nil { + self.topContentContainerNode.view.addSubview(view) + } + view.bounds = CGRect(origin: .zero, size: cancelButtonFrame.size) + view.center = cancelButtonFrame.center + } + + let switchThemeButtonSize = self.switchThemeButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: barButtonSize, + backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: self.presentationData.theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "switchTheme", component: AnyComponent( + Rectangle(color: .clear) + )), + action: { [weak self] _ in + self?.switchThemePressed() + } + )), + environment: {}, + containerSize: barButtonSize + ) + let switchThemeButtonFrame = CGRect(origin: CGPoint(x: contentFrame.width - switchThemeButtonSize.width - 16.0, y: 16.0), size: switchThemeButtonSize) + if let view = self.switchThemeButton.view { + if view.superview == nil { + self.topContentContainerNode.view.addSubview(view) + self.topContentContainerNode.view.bringSubviewToFront(self.animationContainerNode.view) + } + view.bounds = CGRect(origin: .zero, size: switchThemeButtonFrame.size) + view.center = switchThemeButtonFrame.center + } + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.scrollNodeContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height + 2000.0))) let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 19.0 + UIScreenPixel), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 36.0 - titleSize.height / 2.0), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) let segmentedSize = self.segmentedNode.updateLayout(.sizeToFit(maximumWidth: width - 90.0, minimumWidth: 160.0, height: 32.0), transition: transition) @@ -1498,20 +1528,20 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg let switchThemeSize = CGSize(width: 44.0, height: 44.0) let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize) - transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) - transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) + transition.updateFrame(node: self.switchThemeButtonNode, frame: switchThemeFrame) + transition.updateFrame(node: self.animationContainerNode, frame: switchThemeButtonFrame.insetBy(dx: 5.0, dy: 5.0)) transition.updateFrameAsPositionAndBounds(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size)) let cancelSize = CGSize(width: 44.0, height: 44.0) let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) - transition.updateFrame(node: self.cancelButton, frame: cancelFrame) + transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame) - let buttonInset: CGFloat = 16.0 + let buttonInset: CGFloat = 30.0 let scanButtonHeight = self.scanButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: scanButtonHeight)) + transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom, width: contentFrame.width, height: scanButtonHeight)) let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight)) + transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom, width: contentFrame.width, height: doneButtonHeight)) transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) @@ -1523,7 +1553,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg let contentSize = CGSize(width: contentFrame.width, height: 120.0) self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) - self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 6.0) + self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 12.0) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.contentNode.updateLayout(size: layout.size, topInset: 44.0, bottomInset: contentHeight, transition: transition) diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 48dc56d531..cdf93e25d7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -18,6 +18,10 @@ import UIKitRuntimeUtils public final class GiftCompositionComponent: Component { public class ExternalState { public fileprivate(set) var previewPatternColor: UIColor? + public fileprivate(set) var previewModel: StarGift.UniqueGift.Attribute? + public fileprivate(set) var previewBackdrop: StarGift.UniqueGift.Attribute? + public fileprivate(set) var previewSymbol: StarGift.UniqueGift.Attribute? + public init() { self.previewPatternColor = nil } @@ -671,18 +675,23 @@ public final class GiftCompositionComponent: Component { loop = true self.stopSpinIfNeeded() - if self.previewModels.isEmpty { + if self.previewModels.isEmpty || sampleAttributes.count == 3 { var models: [StarGift.UniqueGift.Attribute] = [] var patterns: [StarGift.UniqueGift.Attribute] = [] var backdrops: [StarGift.UniqueGift.Attribute] = [] for attribute in sampleAttributes { switch attribute { - case .model: models.append(attribute) + case .model: models.append(attribute) case .pattern: patterns.append(attribute) case .backdrop: backdrops.append(attribute) default: break } } + + if self.previewModels != models && sampleAttributes.count == 3 { + self.animatePreviewTransition = true + } + self.previewModels = models self.previewPatterns = patterns self.previewBackdrops = backdrops @@ -707,19 +716,22 @@ public final class GiftCompositionComponent: Component { } if case let .model(_, file, _) = self.previewModels[Int(self.previewModelIndex)] { animationFile = file + component.externalState?.previewModel = self.previewModels[Int(self.previewModelIndex)] } if case let .pattern(_, file, _) = self.previewPatterns[Int(self.previewPatternIndex)] { patternFile = file files[file.fileId.id] = file + component.externalState?.previewSymbol = self.previewPatterns[Int(self.previewPatternIndex)] } if case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] { backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue)) secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue)) patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue)) + component.externalState?.previewBackdrop = self.previewBackdrops[Int(self.previewBackdropIndex)] } } - if self.previewTimer == nil { + if self.previewTimer == nil && sampleAttributes.count > 3 { self.previewTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: true, completion: { [weak self] in guard let self, !self.previewModels.isEmpty else { return } self.previewModelIndex = (self.previewModelIndex + 1) % Int32(self.previewModels.count) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 6fc7466c93..4ceb2486c4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -30,6 +30,7 @@ public final class GiftItemComponent: Component { case starGift(gift: StarGift.Gift, price: String) case uniqueGift(gift: StarGift.UniqueGift, price: String?) case auction(gift: StarGift.Gift, centerColor: UIColor, edgeColor: UIColor, endTime: Int32) + case preview(attributes: [StarGift.UniqueGift.Attribute], rarity: Int32) } public struct Ribbon: Equatable { @@ -144,6 +145,7 @@ public final class GiftItemComponent: Component { case buttonIcon case tableIcon case header + case upgradePreview } let context: AccountContext @@ -421,6 +423,14 @@ public final class GiftItemComponent: Component { size = availableSize iconSize = CGSize(width: 106.0, height: 106.0) cornerRadius = 16.0 + case .upgradePreview: + size = availableSize + if case let .preview(attributes, _) = component.subject, attributes.count == 2 { + iconSize = CGSize(width: 60.0, height: 60.0) + } else { + iconSize = CGSize(width: 72.0, height: 72.0) + } + cornerRadius = 16.0 } var backgroundSize = size if case .grid = component.mode { @@ -565,6 +575,45 @@ public final class GiftItemComponent: Component { }, queue: Queue.mainQueue()) self.giftAuctionTimer?.start() } + case let .preview(attributes, _): + animationOffset = 16.0 + explicitAnimationOffset = -4.0 + for attribute in attributes { + switch attribute { + case let .model(_, file, _): + animationFile = file + if !self.fetchedFiles.contains(file.fileId.id) { + self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + self.fetchedFiles.insert(file.fileId.id) + } + case let .pattern(_, file, _): + patternFile = file + files[file.fileId.id] = file + case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _): + backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue)) + secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue)) + patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue)) + if let backgroundColor { + placeholderColor = backgroundColor + } + default: + break + } + } + + if animationFile == nil, let patternFile { + animationFile = patternFile + } + + if let animationFile { + emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: animationFile.fileId.id, + file: animationFile + ) + } else { + emoji = nil + } } if [.buttonIcon, .tableIcon].contains(component.mode) { @@ -640,7 +689,62 @@ public final class GiftItemComponent: Component { } } - if case .preview = component.mode { + if case .upgradePreview = component.mode, case let .preview(attributes, rarity) = component.subject { + let isColored = attributes.count > 1 + if let title = component.title { + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.medium(13.0), textColor: isColored ? .white : component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: size.height - 27.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + } + + func formatPercentage(_ value: Float) -> String { + return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + "%" + } + let percentage = Float(rarity) * 0.1 + + let badgeTextSize = self.badgeText.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: formatPercentage(percentage), font: Font.with(size: 11.0, weight: .medium, traits: .monospacedNumbers), textColor: isColored ? .white : component.theme.list.itemSecondaryTextColor))) + ), + environment: {}, + containerSize: availableSize + ) + + let badgeBackgroundSize = CGSize(width: badgeTextSize.width + 12.0, height: 18.0) + let _ = self.badgeBackground.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + RoundedRectangle(color: isColored ? UIColor(white: 0.0, alpha: 0.2) : component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.06), cornerRadius: 9.0) + ), + environment: {}, + containerSize: badgeBackgroundSize + ) + + if let badgeBackgroundView = self.badgeBackground.view, let badgeTextView = self.badgeText.view { + if badgeBackgroundView.superview == nil { + self.addSubview(badgeBackgroundView) + self.addSubview(badgeTextView) + } + badgeTextView.frame = CGRect(origin: CGPoint(x: size.width - 12.0 - badgeTextSize.width, y: 9.0), size: badgeTextSize) + badgeBackgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(badgeTextView.frame.center.x - badgeBackgroundSize.width / 2.0), y: floorToScreenPixels(badgeTextView.frame.center.y - badgeBackgroundSize.height / 2.0)), size: badgeBackgroundSize) + } + } else if case .preview = component.mode { if let title = component.title { let titleSize = self.title.update( transition: transition, @@ -793,7 +897,9 @@ public final class GiftItemComponent: Component { case .auction: buttonColor = .clear price = "" - break + case .preview: + buttonColor = .clear + price = "" } let buttonSize = self.button.update( @@ -1189,9 +1295,19 @@ public final class GiftItemComponent: Component { } switch component.mode { - case .generic, .grid: + case .generic, .grid, .upgradePreview: let lineWidth: CGFloat = 2.0 - let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0) + let selectionFrame = backgroundFrame.insetBy(dx: 2.0, dy: 2.0) + + var cornerRadius: CGFloat = 6.0 + if case .upgradePreview = component.mode { + cornerRadius = 13.0 + } + + var selectionColor = UIColor.white + if case .upgradePreview = component.mode, case let .preview(attributes, _) = component.subject, attributes.count == 1 { + selectionColor = component.theme.list.itemAccentColor + } if component.isSelected { let selectionLayer: SimpleShapeLayer @@ -1207,13 +1323,13 @@ public final class GiftItemComponent: Component { } selectionLayer.fillColor = UIColor.clear.cgColor - selectionLayer.strokeColor = UIColor.white.cgColor + selectionLayer.strokeColor = selectionColor.cgColor selectionLayer.lineWidth = lineWidth selectionLayer.frame = selectionFrame - selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) + selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) if !transition.animation.isImmediate { - let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) + let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2) } @@ -1222,7 +1338,7 @@ public final class GiftItemComponent: Component { } else if let selectionLayer = self.selectionLayer { self.selectionLayer = nil - let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) + let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in selectionLayer.removeFromSuperlayer() diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 350b39f791..c4cde11b0e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -61,6 +61,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/BotPaymentsUI", + "//submodules/TelegramUI/Components/SegmentControlComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 0ec246d3b5..07629d6fb4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -1141,8 +1141,7 @@ private final class GiftAuctionBidScreenComponent: Component { private let sliderPlus = ComponentView() private let badge = ComponentView() - private var liveStreamPerks: [ComponentView] = [] - private var liveStreamMessagePreview: ComponentView? + private var auctionStats: [ComponentView] = [] private let myGifts = ComponentView() @@ -2366,7 +2365,7 @@ private final class GiftAuctionBidScreenComponent: Component { self.badgeStars.update(size: starsRect.size, color: sliderColor, emitterPosition: CGPoint(x: badgeFrame.midX, y: badgeFrame.maxY - 32.0)) } - var perks: [([AnimatedTextComponent.Item], String)] = [] + var auctionStats: [([AnimatedTextComponent.Item], String)] = [] var minBidAnimatedItems: [AnimatedTextComponent.Item] = [] var untilNextDropAnimatedItems: [AnimatedTextComponent.Item] = [] @@ -2458,62 +2457,62 @@ private final class GiftAuctionBidScreenComponent: Component { } } - perks.append(( + auctionStats.append(( minBidAnimatedItems, environment.strings.Gift_AuctionBid_MinimumBid )) - perks.append(( + auctionStats.append(( untilNextDropAnimatedItems, environment.strings.Gift_AuctionBid_UntilNext )) - perks.append(( + auctionStats.append(( dropsLeftAnimatedItems, environment.strings.Gift_AuctionBid_Left )) contentHeight += 54.0 - let perkHeight: CGFloat = 60.0 - let perkSpacing: CGFloat = 10.0 - let perkWidth: CGFloat = floor((availableSize.width - sideInset * 2.0 - perkSpacing * CGFloat(perks.count - 1)) / CGFloat(perks.count)) + let statSpacing: CGFloat = 10.0 + let statWidth: CGFloat = floor((availableSize.width - sideInset * 2.0 - statSpacing * CGFloat(auctionStats.count - 1)) / CGFloat(auctionStats.count)) + let statHeight: CGFloat = 60.0 - for i in 0 ..< perks.count { - var perkFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (perkWidth + perkSpacing), y: contentHeight), size: CGSize(width: perkWidth, height: perkHeight)) - if i == perks.count - 1 { - perkFrame.size.width = max(0.0, availableSize.width - sideInset - perkFrame.minX) + for i in 0 ..< auctionStats.count { + var statFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (statWidth + statSpacing), y: contentHeight), size: CGSize(width: statWidth, height: statHeight)) + if i == auctionStats.count - 1 { + statFrame.size.width = max(0.0, availableSize.width - sideInset - statFrame.minX) } - let perkView: ComponentView - if self.liveStreamPerks.count > i { - perkView = self.liveStreamPerks[i] + let statView: ComponentView + if self.auctionStats.count > i { + statView = self.auctionStats[i] } else { - perkView = ComponentView() - self.liveStreamPerks.append(perkView) + statView = ComponentView() + self.auctionStats.append(statView) } - let perk = perks[i] - let _ = perkView.update( + let stat = auctionStats[i] + let _ = statView.update( transition: transition, component: AnyComponent(AuctionStatComponent( context: component.context, - gift: i == perks.count - 1 ? component.auctionContext.gift : nil, - title: perk.0, - subtitle: perk.1, + gift: i == auctionStats.count - 1 ? component.auctionContext.gift : nil, + title: stat.0, + subtitle: stat.1, small: false, theme: environment.theme )), environment: {}, - containerSize: perkFrame.size + containerSize: statFrame.size ) - if let perkComponentView = perkView.view { + if let perkComponentView = statView.view { if perkComponentView.superview == nil { self.scrollContentView.addSubview(perkComponentView) } - transition.setFrame(view: perkComponentView, frame: perkFrame) + transition.setFrame(view: perkComponentView, frame: statFrame) } } - contentHeight += perkHeight + contentHeight += statHeight contentHeight += 24.0 let acquiredGiftsCount = self.giftAuctionState?.myState.acquiredCount ?? 0 diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index b461cf3f60..9348390dbc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -71,7 +71,10 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { fileprivate var giftAuctionAcquiredGifts: [GiftAuctionAcquiredGift] = [] private var giftAuctionAcquiredGiftsPromise = ValuePromise<[GiftAuctionAcquiredGift]>() private var giftAuctionAcquiredGiftsDisposable = MetaDisposable() - + + private(set) var giftValueInfo: StarGift.UniqueGift.ValueInfo? + private var giftValueInfoDisposable: Disposable? + var cachedStarImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? @@ -102,6 +105,17 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { if let acquiredCount = auctionState?.myState.acquiredCount, acquiredCount > (previousState?.myState.acquiredCount ?? 0) { self.loadAcquiredGifts() } + + if !"".isEmpty, self.giftValueInfoDisposable == nil, let auctionState, case let .generic(gift) = auctionState.gift, case .finished = auctionState.auctionState, let slug = gift.auctionSlug { + self.giftValueInfoDisposable = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: "\(slug)-1") + |> deliverOnMainQueue).start(next: { [weak self] valueInfo in + guard let self else { + return + } + self.giftValueInfo = valueInfo + self.updated(transition: .easeInOut(duration: 0.25)) + }) + } }) self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in @@ -114,6 +128,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { self.disposable?.dispose() self.giftAuctionAcquiredGiftsDisposable.dispose() self.giftAuctionTimer?.invalidate() + self.giftValueInfoDisposable?.dispose() } func loadAcquiredGifts() { @@ -291,6 +306,26 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { controller.present(shareController, in: .window(.root)) } + func openGiftResale() { + guard let controller = self.getController() as? GiftAuctionViewScreen, let gift = self.giftAuctionState?.gift, case let .generic(gift) = gift else { + return + } + let storeController = self.context.sharedContext.makeGiftStoreController( + context: self.context, + peerId: self.context.account.peerId, + gift: gift + ) + controller.push(storeController) + } + + func openGiftFragmentResale() { + guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController, let url = self.giftValueInfo?.fragmentListedUrl else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + func morePressed(view: UIView, gesture: ContextGesture?) { guard let controller = self.getController() as? GiftAuctionViewScreen else { return @@ -367,8 +402,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let button = Child(ButtonComponent.self) let acquiredButton = Child(PlainButtonComponent.self) -// let telegramSaleButton = Child(PlainButtonComponent.self) -// let fragmentSaleButton = Child(PlainButtonComponent.self) + let telegramSaleButton = Child(PlainButtonComponent.self) + let fragmentSaleButton = Child(PlainButtonComponent.self) let moreButtonPlayOnce = ActionSlot() @@ -678,8 +713,13 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { guard let state else { return } + #if DEBUG + let giftController = GiftUpgradePreviewScreen(context: component.context) + environment.controller()?.push(giftController) + #else let giftController = GiftAuctionAcquiredScreen(context: component.context, gift: component.auctionContext.gift, acquiredGifts: state.giftAuctionAcquiredGifts) environment.controller()?.push(giftController) + #endif }, animateScale: false), availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), transition: context.transition @@ -692,6 +732,98 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { hasAdditionalButtons = true } + if let giftValueInfo = state.giftValueInfo, case let .generic(gift) = component.auctionContext.gift { + if let listedCount = giftValueInfo.listedCount, listedCount > 0 { + originY += 5.0 + + let telegramSaleButton = telegramSaleButton.update( + component: PlainButtonComponent(content: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "count", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "spacing", component: AnyComponent( + Rectangle(color: .clear, width: 8.0, height: 1.0) + )), + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: .starGift(gift: gift, price: ""), + mode: .buttonIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnTelegram)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent( + BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) + )) + ], spacing: 0.0) + ), action: { [weak state] in + guard let state else { + return + } + state.openGiftResale() + }, animateScale: false), + availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(telegramSaleButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + telegramSaleButton.size.height / 2.0))) + originY += telegramSaleButton.size.height + originY += 12.0 + + hasAdditionalButtons = true + } + + if let listedCount = giftValueInfo.fragmentListedCount, listedCount > 0 { + let fragmentSaleButton = fragmentSaleButton.update( + component: PlainButtonComponent(content: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "count", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "spacing", component: AnyComponent( + Rectangle(color: .clear, width: 8.0, height: 1.0) + )), + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: .starGift(gift: gift, price: ""), + mode: .buttonIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnFragment)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent( + BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) + )) + ], spacing: 0.0) + ), action: { [weak state] in + guard let state else { + return + } + state.openGiftFragmentResale() + }, animateScale: false), + availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(fragmentSaleButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + fragmentSaleButton.size.height / 2.0))) + originY += fragmentSaleButton.size.height + originY += 12.0 + + hasAdditionalButtons = true + } + } + if hasAdditionalButtons { originY += 21.0 } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift new file mode 100644 index 0000000000..98942c36a1 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift @@ -0,0 +1,485 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode +import Markdown +import GiftItemComponent +import ChatMessagePaymentAlertController +import ActivityIndicator +import TooltipUI +import MultilineTextComponent +import TelegramStringFormatting + +private final class GiftOfferAlertContentNode: AlertContentNode { + private let context: AccountContext + private let strings: PresentationStrings + private var presentationTheme: PresentationTheme + private let title: String + private let text: String + private let gift: StarGift.UniqueGift + + private let titleNode: ASTextNode + private let giftView = ComponentView() + private let textNode: ASTextNode + private let arrowNode: ASImageNode + private let avatarNode: AvatarNode + private let tableView = ComponentView() + + private let modelButtonTag = GenericComponentViewTag() + private let backdropButtonTag = GenericComponentViewTag() + private let symbolButtonTag = GenericComponentViewTag() + + fileprivate var getController: () -> ViewController? = { return nil} + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var activityIndicator: ActivityIndicator? + + private var validLayout: CGSize? + + var inProgress = false { + didSet { + if let size = self.validLayout { + let _ = self.updateLayout(size: size, transition: .immediate) + } + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init( + context: AccountContext, + theme: AlertControllerTheme, + ptheme: PresentationTheme, + strings: PresentationStrings, + gift: StarGift.UniqueGift, + peer: EnginePeer, + title: String, + text: String, + actions: [TextAlertAction] + ) { + self.context = context + self.strings = strings + self.presentationTheme = ptheme + self.title = title + self.text = text + self.gift = gift + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 0 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + + self.avatarNode = 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.arrowNode) + self.addSubnode(self.avatarNode) + + 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: peer) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor) + 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.withAlphaComponent(0.9)) + + 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) + } + } + + fileprivate func dismissAllTooltips() { + guard let controller = self.getController() else { + return + } + controller.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + }) + controller.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + return true + }) + } + + func showAttributeInfo(tag: Any, text: String) { + guard let controller = self.getController() else { + return + } + self.dismissAllTooltips() + + guard let sourceView = self.tableView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else { + return + } + + let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize()) + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + }) + controller.present(tooltipController, in: .current) + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 310.0) + + let strings = self.strings + + 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 giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) + + let _ = self.giftView.update( + transition: .immediate, + component: AnyComponent( + GiftItemComponent( + context: self.context, + theme: self.presentationTheme, + strings: strings, + peer: nil, + subject: .uniqueGift(gift: self.gift, price: nil), + mode: .thumbnail + ) + ), + environment: {}, + containerSize: avatarSize + ) + if let view = self.giftView.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.frame = giftFrame + } + + 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 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) + + origin.y += avatarSize.height + 17.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 + 5.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 + + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + 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) + + let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + + let tableFont = Font.regular(15.0) + let tableTextColor = self.presentationTheme.list.itemPrimaryTextColor + + var tableItems: [TableComponent.Item] = [] + let order: [StarGift.UniqueGift.Attribute.AttributeType] = [ + .model, .pattern, .backdrop, .originalInfo + ] + + var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:] + for attribute in self.gift.attributes { + attributeMap[attribute.attributeType] = attribute + } + + for type in order { + if let attribute = attributeMap[type] { + let id: String? + let title: String? + let value: NSAttributedString + let percentage: Float? + let tag: AnyObject? + + switch attribute { + case let .model(name, _, rarity): + id = "model" + title = strings.Gift_Unique_Model + value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) + percentage = Float(rarity) * 0.1 + tag = self.modelButtonTag + case let .backdrop(name, _, _, _, _, _, rarity): + id = "backdrop" + title = strings.Gift_Unique_Backdrop + value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) + percentage = Float(rarity) * 0.1 + tag = self.backdropButtonTag + case let .pattern(name, _, rarity): + id = "pattern" + title = strings.Gift_Unique_Symbol + value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) + percentage = Float(rarity) * 0.1 + tag = self.symbolButtonTag + case .originalInfo: + continue + } + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent( + MultilineTextComponent(text: .plain(value)) + ) + ) + ) + if let percentage, let tag { + items.append(AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: self.context, + text: formatPercentage(percentage), + color: self.presentationTheme.list.itemAccentColor + )), + action: { [weak self] in + self?.showAttributeInfo(tag: tag, text: strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string) + } + ).tagged(tag)) + )) + } + let itemComponent = AnyComponent( + HStack(items, spacing: 4.0) + ) + + tableItems.append(.init( + id: id, + title: title, + hasBackground: false, + component: itemComponent + )) + } + } + + if let valueAmount = self.gift.valueAmount, let valueCurrency = self.gift.valueCurrency { + tableItems.append(.init( + id: "fiatValue", + title: strings.Gift_Unique_Value, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))) + ), + insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) + )) + } + + let tableSize = self.tableView.update( + transition: .immediate, + component: AnyComponent( + TableComponent( + theme: self.presentationTheme, + items: tableItems, + semiTransparent: true + ) + ), + environment: {}, + containerSize: CGSize(width: contentWidth - 32.0, height: size.height) + ) + let tableFrame = CGRect(origin: CGPoint(x: 16.0, y: avatarSize.height + titleSize.height + textSize.height + 60.0), size: tableSize) + if let view = self.tableView.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.frame = tableFrame + } + + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + tableSize.height + actionsHeight + 40.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 + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + do { + 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 + do { + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + do { + 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 + } + + if self.inProgress { + let activityIndicator: ActivityIndicator + if let current = self.activityIndicator { + activityIndicator = current + } else { + activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false)) + self.addSubnode(activityIndicator) + } + + if let actionNode = self.actionNodes.first { + actionNode.isUserInteractionEnabled = false + actionNode.isHidden = false + + let indicatorSize = CGSize(width: 22.0, height: 22.0) + transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + } + } + + return resultSize + } +} + +public func giftOfferAlertController( + context: AccountContext, + gift: StarGift.UniqueGift, + peer: EnginePeer, + amount: CurrencyAmount, + commit: @escaping () -> Void +) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let title = "Confirm Sale" + let buttonText: String = "Confirm Sale" + + let priceString: String + switch amount.currency { + case .stars: + priceString = "\(amount.amount) Stars" + case .ton: + priceString = "\(amount.amount) TON" + } + + let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let finalPriceString: String + switch amount.currency { + case .stars: + let starsValue = Int32(floor(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionStarsPermille) / 1000.0)) + finalPriceString = "\(starsValue) Stars" + case .ton: + let tonValue = Int64(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionTonPermille) / 1000.0) + finalPriceString = formatTonAmountText(tonValue, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: 3) + " TON" + } + + let giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: presentationData.dateTimeFormat))" + let text = "Do you want to sell **\(giftTitle)** to \(peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)) for **\(priceString)**? You'll receive **\(finalPriceString)** after fees." + + + var contentNode: GiftOfferAlertContentNode? + var dismissImpl: ((Bool) -> Void)? + let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in + contentNode?.inProgress = true + commit() + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + })] + + contentNode = GiftOfferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions) + + let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: nil, chatPeerId: context.account.peerId, showBalance: false) + contentNode?.getController = { [weak controller] in + return controller + } + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index 45585138f6..a58d8bf55c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -344,7 +344,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode { id: "fiatValue", title: strings.Gift_Unique_Value, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "≈\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))) ), insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) )) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift index e02517851e..43a1b83264 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift @@ -16,6 +16,7 @@ import ButtonComponent import PlainButtonComponent import GiftItemComponent import AccountContext +import GlassBarButtonComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -62,8 +63,7 @@ private final class SheetContent: CombinedComponent { } static var body: Body { - let closeButton = Child(Button.self) - + let closeButton = Child(GlassBarButtonComponent.self) let title = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self) let gifts = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) @@ -79,7 +79,6 @@ private final class SheetContent: CombinedComponent { let theme = environment.theme let strings = environment.strings - let sideInset: CGFloat = 16.0 + environment.safeInsets.left let textSideInset: CGFloat = 32.0 + environment.safeInsets.left let titleFont = Font.semibold(17.0) @@ -87,20 +86,29 @@ private final class SheetContent: CombinedComponent { let textColor = theme.actionSheet.primaryTextColor let secondaryTextColor = theme.actionSheet.secondaryTextColor - var contentSize = CGSize(width: context.availableSize.width, height: 10.0) + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)), - action: { [weak component] in - component?.dismiss() + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { _ in + component.dismiss() } ), - availableSize: CGSize(width: 100.0, height: 30.0), + availableSize: CGSize(width: 40.0, height: 40.0), transition: .immediate ) context.add(closeButton - .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 28.0)) + .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 36.0)) ) let title = title.update( @@ -234,6 +242,7 @@ private final class SheetContent: CombinedComponent { let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) @@ -256,7 +265,7 @@ private final class SheetContent: CombinedComponent { } } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0), transition: context.transition ) context.add(button @@ -335,6 +344,7 @@ private final class SheetContainerComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, externalState: sheetExternalState, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift new file mode 100644 index 0000000000..2efb882263 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift @@ -0,0 +1,1381 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import ComponentFlow +import AccountContext +import ViewControllerComponent +import TelegramCore +import SwiftSignalKit +import Display +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import ButtonComponent +import PlainButtonComponent +import Markdown +import BundleIconComponent +import TextFormat +import TelegramStringFormatting +import GlassBarButtonComponent +import GiftItemComponent +import EdgeEffect +import AnimatedTextComponent +import SegmentControlComponent +import GiftAnimationComponent +import GlassBackgroundComponent + +private final class GiftUpgradePreviewScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: GiftUpgradePreviewScreenComponent, rhs: GiftUpgradePreviewScreenComponent) -> Bool { + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var containerCornerRadius: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.containerCornerRadius = containerCornerRadius + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + enum SelectedSection { + case models + case backdrops + case symbols + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let containerView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let glassContainerView: GlassBackgroundContainerView + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let backgroundHandleView: UIImageView + + private let header = ComponentView() + private let closeButton = ComponentView() + private let playbackButton = ComponentView() + + private let title = ComponentView() + private let subtitle = ComponentView() + + private var attributeInfos: [ComponentView] = [] + + private let segmentControl = ComponentView() + private let descriptionText = ComponentView() + + private var giftItems: [AnyHashable: ComponentView] = [:] + + private var selectedSection: SelectedSection = .models + + private let giftCompositionExternalState = GiftCompositionComponent.ExternalState() + + fileprivate var starGiftsContext: ResaleGiftsContext? + fileprivate var starGiftsState: ResaleGiftsContext.State? + fileprivate var starGiftsDisposable: Disposable? + + private var previewTimer: SwiftSignalKit.Timer? + private var isPlaying = true + private var showRandomizeTip = false + + private var previewModelIndex: Int = 0 + private var previewBackdropIndex: Int = 0 + private var previewSymbolIndex: Int = 0 + + private var previewModels: [StarGift.UniqueGift.Attribute] = [] + private var previewBackdrops: [StarGift.UniqueGift.Attribute] = [] + private var previewSymbols: [StarGift.UniqueGift.Attribute] = [] + + private var selectedModel: StarGift.UniqueGift.Attribute? + private var selectedBackdrop: StarGift.UniqueGift.Attribute? + private var selectedSymbol: StarGift.UniqueGift.Attribute? + + private var modelCount: Int32 = 0 + private var backdropCount: Int32 = 0 + private var symbolCount: Int32 = 0 + + private var ignoreScrolling: Bool = false + + private var component: GiftUpgradePreviewScreenComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var environment: ViewControllerComponentContainer.Environment? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.dimView = UIView() + self.containerView = UIView() + + self.containerView.clipsToBounds = true + self.containerView.layer.cornerRadius = 40.0 + self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 40.0 + + self.backgroundHandleView = UIImageView() + + self.navigationBarContainer = SparseContainerView() + + self.glassContainerView = GlassBackgroundContainerView() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.addSubview(self.containerView) + self.containerView.layer.addSublayer(self.backgroundLayer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.containerView.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.containerView.addSubview(self.navigationBarContainer) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.starGiftsDisposable?.dispose() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func updateScrolling(transition: ComponentTransition) { + guard let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + var topOffsetFraction = self.scrollView.bounds.minY / 100.0 + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width + let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0 + let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius + + let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction + let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) + let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction + + var containerTransform = CATransform3DIdentity + containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) + containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) + transition.setTransform(view: self.containerView, transform: containerTransform) + transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) + + self.updateItems(transition: transition) + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + + private func timerTick() { + guard !self.previewModels.isEmpty else { return } + self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count + + let previousSymbolIndex = self.previewSymbolIndex + var randomSymbolIndex = previousSymbolIndex + while randomSymbolIndex == previousSymbolIndex && !self.previewSymbols.isEmpty { + randomSymbolIndex = Int.random(in: 0 ..< self.previewSymbols.count) + } + if !self.previewSymbols.isEmpty { self.previewSymbolIndex = randomSymbolIndex } + + let previousBackdropIndex = self.previewBackdropIndex + var randomBackdropIndex = previousBackdropIndex + while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty { + randomBackdropIndex = Int.random(in: 0 ..< self.previewBackdrops.count) + } + if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex } + + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + + private func updateTimer() { + if self.isPlaying { + self.previewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in + guard let self else { + return + } + self.timerTick() + }, queue: Queue.mainQueue()) + self.previewTimer?.start() + } else { + self.previewTimer?.invalidate() + self.previewTimer = nil + } + } + + private var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] + private func updateEffectiveGifts() { + guard let starGiftsState = self.starGiftsState else { + return + } + + var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] + switch self.selectedSection { + case .models: + let models = Array(starGiftsState.attributes.filter({ attribute in + if case .model = attribute { + return true + } else { + return false + } + })) + for model in models { + effectiveGifts.append([model]) + } + case .backdrops: + let selectedModel = self.selectedModel ?? self.previewModels[self.previewModelIndex] + let selectedSymbol = self.selectedSymbol ?? self.previewSymbols[self.previewSymbolIndex] + let backdrops = Array(starGiftsState.attributes.filter({ attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + })) + for backdrop in backdrops { + effectiveGifts.append([ + selectedModel, + backdrop, + selectedSymbol + ]) + } + case .symbols: + let selectedBackdrop = self.selectedBackdrop ?? self.previewBackdrops[self.previewBackdropIndex] + let symbols = Array(starGiftsState.attributes.filter({ attribute in + if case .pattern = attribute { + return true + } else { + return false + } + })) + for symbol in symbols { + effectiveGifts.append([ + selectedBackdrop, + symbol + ]) + } + } + self.effectiveGifts = effectiveGifts + } + + private func updateItems(transition: ComponentTransition) { + guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { + return + } + + let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0) + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let optionSpacing: CGFloat = 10.0 + let optionWidth = (itemLayout.containerSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 + let optionSize = CGSize(width: optionWidth, height: 126.0) + + let topInset: CGFloat = 393.0 + + var validIds: [AnyHashable] = [] + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + 9.0), size: optionSize) + + for attributeList in self.effectiveGifts { + var isVisible = false + if visibleBounds.intersects(itemFrame) { + isVisible = true + } + + var itemId = "" + var title = "" + var rarity: Int32 = 0 + + var modelAttribute: StarGift.UniqueGift.Attribute? + var backdropAttribute: StarGift.UniqueGift.Attribute? + var symbolAttribute: StarGift.UniqueGift.Attribute? + + switch self.selectedSection { + case .models: + itemId += "models_" + case .backdrops: + itemId += "backdrops_" + case .symbols: + itemId += "symbols_" + } + + var isSelected = false + for attribute in attributeList { + switch attribute { + case let .model(name, _, rarityValue): + itemId += name + if self.selectedSection == .models { + title = name + rarity = rarityValue + modelAttribute = attribute + + isSelected = self.selectedModel == attribute + } + case let .backdrop(name, _, _, _, _, _, rarityValue): + itemId += name + if self.selectedSection == .backdrops { + title = name + rarity = rarityValue + backdropAttribute = attribute + + isSelected = self.selectedBackdrop == attribute + } + case let .pattern(name, _, rarityValue): + itemId += name + if self.selectedSection == .symbols { + title = name + rarity = rarityValue + symbolAttribute = attribute + + isSelected = self.selectedSymbol == attribute + } + default: + break + } + } + + if isVisible { + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.giftItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.giftItems[itemId] = visibleItem + } + + let subject: GiftItemComponent.Subject = .preview(attributes: attributeList, rarity: rarity) + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + GiftItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: subject, + title: title, + ribbon: nil, + isSelected: isSelected, + mode: .upgradePreview + ) + ), + effectAlignment: .center, + action: { [weak self] in + guard let self, let state = self.state else { + return + } + if self.isPlaying { + self.isPlaying = false + self.showRandomizeTip = true + Queue.mainQueue().after(2.0) { + if self.showRandomizeTip { + self.showRandomizeTip = false + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + } + } + + switch self.selectedSection { + case .models: + self.selectedModel = modelAttribute + case .backdrops: + self.selectedBackdrop = backdropAttribute + case .symbols: + self.selectedSymbol = symbolAttribute + } + + state.updated(transition: .easeInOut(duration: 0.25)) + }, + animateAlpha: false + ) + ), + environment: {}, + containerSize: optionSize + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.scrollContentView.addSubview(itemView) + + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + itemFrame.origin.x += itemFrame.width + optionSpacing + if itemFrame.maxX > itemLayout.containerSize.width { + itemFrame.origin.x = sideInset + itemFrame.origin.y += optionSize.height + optionSpacing + } + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.giftItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.giftItems.removeValue(forKey: id) + } + } + + func update(component: GiftUpgradePreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.updateTimer() + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let fillingSize: CGFloat + if case .regular = environment.metrics.widthClass { + fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0 + } else { + fillingSize = min(availableSize.width, environment.deviceMetrics.screenSize.width) - environment.safeInsets.left * 2.0 + } + let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + let sideInset: CGFloat = rawSideInset + 16.0 + + if self.component == nil { + let giftIds: [Int64] = [ + 6014675319464657779, + 6042113507581755979, + 5936013938331222567 + ] + + self.starGiftsContext = ResaleGiftsContext(account: component.context.account, giftId: giftIds.randomElement()!) + self.starGiftsDisposable = (self.starGiftsContext!.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + let isFirstTime = self.starGiftsState?.attributes.isEmpty ?? true + self.starGiftsState = state + + var modelCount: Int32 = 0 + var backdropCount: Int32 = 0 + var symbolCount: Int32 = 0 + for attribute in state.attributes { + switch attribute { + case .model: + modelCount += 1 + case .backdrop: + backdropCount += 1 + case .pattern: + symbolCount += 1 + default: + break + } + } + self.modelCount = modelCount + self.backdropCount = backdropCount + self.symbolCount = symbolCount + + if isFirstTime { + let randomModels = Array(state.attributes.filter({ attribute in + if case .model = attribute { + return true + } else { + return false + } + }).shuffled().prefix(15)) + self.previewModels = randomModels + + let randomBackdrops = Array(state.attributes.filter({ attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }).shuffled()) + self.previewBackdrops = randomBackdrops + + let randomSymbols = Array(state.attributes.filter({ attribute in + if case .pattern = attribute { + return true + } else { + return false + } + }).shuffled().prefix(15)) + self.previewSymbols = randomSymbols + + self.updateEffectiveGifts() + } + + self.state?.updated(transition: .immediate) + }) + } + + self.component = component + self.state = state + self.environment = environment + + let theme = environment.theme.withModalBlocksBackground() + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = theme.list.blocksBackgroundColor.cgColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var buttonColor: UIColor = .white.withAlphaComponent(0.1) + var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4) + + var attributes: [StarGift.UniqueGift.Attribute] = [] + if !self.previewModels.isEmpty { + if self.isPlaying { + attributes.append(self.previewModels[self.previewModelIndex]) + attributes.append(self.previewBackdrops[self.previewBackdropIndex]) + attributes.append(self.previewSymbols[self.previewSymbolIndex]) + } else { + if self.selectedModel == nil { + self.selectedModel = self.previewModels[self.previewModelIndex] + } + if self.selectedBackdrop == nil { + self.selectedBackdrop = self.previewBackdrops[self.previewBackdropIndex] + } + if self.selectedSymbol == nil { + self.selectedSymbol = self.previewSymbols[self.previewSymbolIndex] + } + if let model = self.selectedModel { + attributes.append(model) + } + if let backdrop = self.selectedBackdrop { + attributes.append(backdrop) + } + if let symbol = self.selectedSymbol { + attributes.append(symbol) + } + } + } + + if let backdropAttribute = attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { + let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) + let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) + buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8) + + secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) + } + + var contentHeight: CGFloat = 0.0 + let headerSize = self.header.update( + transition: transition, + component: AnyComponent(GiftCompositionComponent( + context: component.context, + theme: environment.theme, + subject: .preview(attributes), + animationOffset: CGPoint(x: 0.0, y: 20.0), + animationScale: nil, + displayAnimationStars: false, + revealedAttributes: Set(), + externalState: self.giftCompositionExternalState, + requestUpdate: { [weak state] transition in + state?.updated(transition: transition) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 300.0), + ) + let headerFrame = CGRect(origin: CGPoint(x: floor((fillingSize - headerSize.width) * 0.5), y: 0.0), size: headerSize) + if let headerView = self.header.view { + if headerView.superview == nil { + headerView.isUserInteractionEnabled = false + headerView.clipsToBounds = true + headerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + headerView.layer.cornerRadius = 38.0 + self.navigationBarContainer.addSubview(headerView) + } + transition.setFrame(view: headerView, frame: headerFrame) + } + + contentHeight += headerSize.height + + var titleText: String = "" + if let gift = self.starGiftsState?.gifts.first, case let .unique(gift) = gift { + titleText = gift.title + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleText, font: Font.semibold(20.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((fillingSize - titleSize.width) * 0.5), y: contentHeight - 124.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.navigationBarContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let subtitleSize = self.subtitle.update( + transition: .spring(duration: 0.2), + component: AnyComponent(AnimatedTextComponent( + font: Font.regular(14.0), + color: secondaryTextColor, + items: [ + AnimatedTextComponent.Item(id: self.isPlaying ? "random" : "selected", content: .text(self.isPlaying ? "Random" : "Selected")), + AnimatedTextComponent.Item(id: "traits", content: .text(" Traits")) + ], + noDelay: true, + blur: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((fillingSize - subtitleSize.width) * 0.5), y: contentHeight - 97.0), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.navigationBarContainer.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + + + let attributeSpacing: CGFloat = 10.0 + let attributeWidth: CGFloat = floor((fillingSize - sideInset * 2.0 - attributeSpacing * CGFloat(attributes.count - 1)) / CGFloat(attributes.count)) + let attributeHeight: CGFloat = 45.0 + + for i in 0 ..< attributes.count { + var attributeFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (attributeWidth + attributeSpacing), y: contentHeight - 60.0), size: CGSize(width: attributeWidth, height: attributeHeight)) + if i == attributes.count - 1 { + attributeFrame.size.width = max(0.0, availableSize.width - sideInset - attributeFrame.minX) + } + let attributeInfo: ComponentView + if self.attributeInfos.count > i { + attributeInfo = self.attributeInfos[i] + } else { + attributeInfo = ComponentView() + self.attributeInfos.append(attributeInfo) + } + let attribute = attributes[i] + let _ = attributeInfo.update( + transition: transition, + component: AnyComponent(AttributeInfoComponent( + strings: environment.strings, + backgroundColor: buttonColor, + secondaryTextColor: secondaryTextColor, + attribute: attribute + )), + environment: {}, + containerSize: attributeFrame.size + ) + if let attributeInfoView = attributeInfo.view { + if attributeInfoView.superview == nil { + self.navigationBarContainer.addSubview(attributeInfoView) + } + transition.setFrame(view: attributeInfoView, frame: attributeFrame) + } + } + + contentHeight += 16.0 + + let segmentedSize = self.segmentControl.update( + transition: transition, + component: AnyComponent(SegmentControlComponent( + theme: environment.theme, + items: [ + SegmentControlComponent.Item(id: AnyHashable(SelectedSection.models), title: "Models"), + SegmentControlComponent.Item(id: AnyHashable(SelectedSection.backdrops), title: "Backgrounds"), + SegmentControlComponent.Item(id: AnyHashable(SelectedSection.symbols), title: "Symbols") + ], + selectedId: "models", + action: { [weak self] id in + guard let self, let id = id.base as? SelectedSection else { + return + } + self.selectedSection = id + self.isPlaying = false + + self.updateEffectiveGifts() + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) //.withUserData(AnimationHint(value: .modeChanged))) + })), + environment: {}, + containerSize: CGSize(width: fillingSize - 8.0 * 2.0, height: 100.0) + ) + let segmentedControlFrame = CGRect(origin: CGPoint(x: floor((fillingSize - segmentedSize.width) * 0.5), y: contentHeight), size: segmentedSize) + if let segmentedControlComponentView = self.segmentControl.view { + if segmentedControlComponentView.superview == nil { + self.scrollContentView.addSubview(segmentedControlComponentView) + } + transition.setFrame(view: segmentedControlComponentView, frame: segmentedControlFrame) + } + contentHeight += segmentedSize.height + contentHeight += 18.0 + + + let descriptionText: String + switch self.selectedSection { + case .models: + descriptionText = "This collection features **\(self.modelCount)** unique models." + case .backdrops: + descriptionText = "This collection features **\(self.backdropCount)** unique backdrops." + case .symbols: + descriptionText = "This collection features **\(self.symbolCount)** unique symbols." + } + + let descriptionFont = Font.regular(13.0) + let descriptionBoldFont = Font.semibold(13.0) + let descriptionTextColor = theme.list.itemSecondaryTextColor + let descriptionMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionTextColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let descriptionSize = self.descriptionText.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: descriptionText, attributes: descriptionMarkdownAttributes) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let descriptionFrame = CGRect(origin: CGPoint(x: floor((fillingSize - descriptionSize.width) * 0.5), y: contentHeight), size: descriptionSize) + if let descriptionView = self.descriptionText.view { + if descriptionView.superview == nil { + self.scrollContentView.addSubview(descriptionView) + } + transition.setFrame(view: descriptionView, frame: descriptionFrame) + } + contentHeight += descriptionSize.height + contentHeight += 26.0 + + + + + //// + //// var validKeys: Set = Set() + //// for auctionState in self.auctionStates { + //// let id = auctionState.gift.giftId + //// validKeys.insert(id) + //// + //// let itemView: ComponentView + //// if let current = self.itemsViews[id] { + //// itemView = current + //// } else { + //// itemView = ComponentView() + //// self.itemsViews[id] = itemView + //// } + //// + //// let itemSize = itemView.update( + //// transition: transition, + //// component: AnyComponent( + //// ActiveAuctionComponent( + //// context: component.context, + //// theme: theme, + //// strings: environment.strings, + //// dateTimeFormat: environment.dateTimeFormat, + //// state: auctionState, + //// currentTime: currentTime, + //// action: { [weak self] in + //// guard let self, let component = self.component else { + //// return + //// } + //// if let giftAuctionsManager = component.context.giftAuctionsManager { + //// let _ = (giftAuctionsManager.auctionContext(for: .giftId(id)) + //// |> deliverOnMainQueue).start(next: { [weak self] auction in + //// guard let self, let component = self.component, let auction, let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else { + //// return + //// } + //// let bidController = component.context.sharedContext.makeGiftAuctionBidScreen(context: component.context, toPeerId: auction.currentBidPeerId ?? component.context.account.peerId, text: nil, entities: nil, hideName: false, auctionContext: auction, acquiredGifts: nil) + //// navigationController.pushViewController(bidController) + //// }) + //// } + //// } + //// ) + //// ), + //// environment: {}, + //// containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + //// ) + //// let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: itemSize) + //// if let view = itemView.view { + //// if view.superview == nil { + //// self.scrollContentView.addSubview(view) + //// } + //// view.frame = itemFrame + //// } + //// contentHeight += itemSize.height + //// contentHeight += 20.0 + //// } + //// contentHeight -= 10.0 + // + // var removeKeys: [Int64] = [] + // for (id, item) in self.itemsViews { + // if !validKeys.contains(id) { + // removeKeys.append(id) + // + // if let itemView = item.view { + // transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + // itemView.removeFromSuperview() + // }) + // } + // } + // } + // for id in removeKeys { + // self.itemsViews.removeValue(forKey: id) + // } + + if self.backgroundHandleView.image == nil { + self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) + } + self.backgroundHandleView.tintColor = UIColor.white.withAlphaComponent(0.4) + let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0)) + if self.backgroundHandleView.superview == nil { + self.navigationBarContainer.addSubview(self.backgroundHandleView) + } + transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame) + + self.glassContainerView.update(size: CGSize(width: fillingSize, height: 64.0), isDark: false, transition: .immediate) + self.glassContainerView.frame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: 64.0)) + + let closeButtonSize = self.closeButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: secondaryTextColor, + isDark: false, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Back", + tintColor: .white + ) + )), + action: { [weak self] _ in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.navigationBarContainer.addSubview(self.glassContainerView) + self.glassContainerView.contentView.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) + } + + var playbackItems: [AnyComponentWithIdentity] = [] + var playbackSize = CGSize(width: 40.0, height: 40.0) + if self.isPlaying { + playbackItems.append(AnyComponentWithIdentity(id: "pause", component: AnyComponent( + BundleIconComponent( + name: "Media Gallery/PictureInPicturePause", + tintColor: .white + ) + ))) + } else { + if self.showRandomizeTip { + playbackItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Randomize Traits", font: Font.semibold(17.0), textColor: .white))) + ))) + playbackSize.width = 186.0 + } + playbackItems.append(AnyComponentWithIdentity(id: "play", component: AnyComponent( + BundleIconComponent( + name: "Media Gallery/PlayButton", + tintColor: .white + ) + ))) + } + + let playbackButtonSize = self.playbackButton.update( + transition: transition, + component: AnyComponent(GlassBarButtonComponent( + size: playbackSize, + backgroundColor: secondaryTextColor, + isDark: false, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "content", component: AnyComponent( + HStack(playbackItems, spacing: 1.0) + )), + action: { [weak self] _ in + guard let self else { + return + } + self.isPlaying = !self.isPlaying + + if !self.isPlaying { + self.showRandomizeTip = true + Queue.mainQueue().after(2.0) { + if self.showRandomizeTip { + self.showRandomizeTip = false + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + } + } else { + self.selectedModel = nil + self.selectedBackdrop = nil + self.selectedSymbol = nil + + self.showRandomizeTip = false + + self.timerTick() + } + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + )), + environment: {}, + containerSize: playbackSize + ) + let playbackButtonFrame = CGRect(origin: CGPoint(x: fillingSize - 16.0 - playbackButtonSize.width, y: 16.0), size: playbackButtonSize) + if let playbackButtonView = self.playbackButton.view { + if playbackButtonView.superview == nil { + playbackButtonView.clipsToBounds = true + self.glassContainerView.contentView.addSubview(playbackButtonView) + } + transition.setFrame(view: playbackButtonView, frame: playbackButtonFrame) + } + + //TODO:release + contentHeight += 126.0 * 17.0 + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + contentHeight += environment.safeInsets.bottom + + var initialContentHeight = contentHeight + let clippingY: CGFloat + + initialContentHeight = contentHeight + + clippingY = availableSize.height + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 38.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height))) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { + let bottomInset: CGFloat = contentHeight - 12.0 + + let layout = ContainerViewLayout( + size: availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class GiftUpgradePreviewScreen: ViewControllerComponentContainer { + private let context: AccountContext + + private var didPlayAppearAnimation: Bool = false + private var isDismissed: Bool = false + + public init(context: AccountContext) { + self.context = context + + super.init(context: context, component: GiftUpgradePreviewScreenComponent( + context: context + ), navigationBarAppearance: .none, theme: .default) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + + if let componentView = self.node.hostView.componentView as? GiftUpgradePreviewScreenComponent.View { + componentView.animateIn() + } + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? GiftUpgradePreviewScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} + +private final class AttributeInfoComponent: Component { + let strings: PresentationStrings + let backgroundColor: UIColor + let secondaryTextColor: UIColor + let attribute: StarGift.UniqueGift.Attribute + + init( + strings: PresentationStrings, + backgroundColor: UIColor, + secondaryTextColor: UIColor, + attribute: StarGift.UniqueGift.Attribute + ) { + self.strings = strings + self.backgroundColor = backgroundColor + self.secondaryTextColor = secondaryTextColor + self.attribute = attribute + } + + static func ==(lhs: AttributeInfoComponent, rhs: AttributeInfoComponent) -> Bool { + if lhs.strings !== rhs.strings { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.secondaryTextColor != rhs.secondaryTextColor { + return false + } + if lhs.attribute != rhs.attribute { + return false + } + return true + } + + final class View: UIView { + let background = SimpleLayer() + let title = ComponentView() + let subtitle = ComponentView() + + let badgeBackground = SimpleLayer() + let badge = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AttributeInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let backgroundFrame = CGRect(origin: CGPoint(), size: availableSize) + if self.background.superlayer == nil { + self.background.cornerRadius = 16.0 + self.background.cornerCurve = .continuous + self.layer.addSublayer(self.background) + + self.badgeBackground.cornerRadius = 9.5 + self.badgeBackground.cornerCurve = .continuous + self.layer.addSublayer(self.badgeBackground) + } + self.background.frame = backgroundFrame + transition.setBackgroundColor(layer: self.background, color: component.backgroundColor) + + let title: String + let subtitle: String + let rarity: Int32 + switch component.attribute { + case let .model(name, _, rarityValue): + title = name + subtitle = "model" + rarity = rarityValue + case let .backdrop(name, _, _, _, _, _, rarityValue): + title = name + subtitle = "backdrop" + rarity = rarityValue + case let .pattern(name, _, rarityValue): + title = name + subtitle = "symbol" + rarity = rarityValue + default: + title = "" + subtitle = "" + rarity = 0 + } + + let _ = rarity + + let titleSize = self.title.update( + transition: .spring(duration: 0.2), + component: AnyComponent(AnimatedTextComponent( + font: Font.semibold(13.0), + color: UIColor.white, + items: [AnimatedTextComponent.Item(id: "title", content: .text(title))], + noDelay: true, + blur: true + )), + environment: {}, + containerSize: CGSize(width: backgroundFrame.size.width - 8.0, height: backgroundFrame.size.height) + ) + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitle, font: Font.regular(11.0), textColor: .white)), + tintColor: component.secondaryTextColor + )), + environment: {}, + containerSize: backgroundFrame.size + ) + + let spacing: CGFloat = 0.0 + let titleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - titleSize.width) * 0.5), y: floor((backgroundFrame.height - titleSize.height - spacing - subtitleSize.height) * 0.5)), size: titleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + spacing), size: subtitleSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + + func formatPercentage(_ value: Float) -> String { + return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + } + let percentage = Float(rarity) * 0.1 + + let badgeSize = self.badge.update( + transition: .spring(duration: 0.2), + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 12.0, weight: .semibold, traits: .monospacedNumbers), + color: UIColor.white, + items: [ + AnimatedTextComponent.Item(id: "value", content: .text(formatPercentage(percentage))), + AnimatedTextComponent.Item(id: "percent", content: .text("%")), + ], + noDelay: true, + blur: true + )), + environment: {}, + containerSize: backgroundFrame.size + ) + let badgeFrame = CGRect(origin: CGPoint(x: backgroundFrame.width - badgeSize.width - 2.0, y: backgroundFrame.minY - 8.0), size: badgeSize) + if let badgeView = self.badge.view { + if badgeView.superview == nil { + self.addSubview(badgeView) + } + transition.setFrame(view: badgeView, frame: badgeFrame) + } + + let badgeBackgroundFrame = badgeFrame.insetBy(dx: -5.5, dy: -2.0) + transition.setFrame(layer: self.badgeBackground, frame: badgeBackgroundFrame) + transition.setBackgroundColor(layer: self.badgeBackground, color: component.backgroundColor.mixedWith(component.secondaryTextColor, alpha: 0.5)) + + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index 2067859721..5586711858 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -370,7 +370,7 @@ private final class GiftValueSheetContent: CombinedComponent { ) )) - let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (≈\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))" + let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (~\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))" let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor) let range = (valueAttributedString.string as NSString).range(of: "⭐️") if range.location != NSNotFound { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ccb673df45..c0ad559a13 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1438,7 +1438,7 @@ private final class GiftViewSheetContent: CombinedComponent { }))) //TODO:localize - if case let .unique(uniqueGift) = arguments.gift, case let .peerId(ownerPeerId) = uniqueGift.owner, ownerPeerId != self.context.account.peerId { + if case let .unique(uniqueGift) = arguments.gift, case let .peerId(ownerPeerId) = uniqueGift.owner, ownerPeerId != self.context.account.peerId, uniqueGift.minOfferStars != nil { items.append(.action(ContextMenuActionItem(text: "Offer to Buy", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -4207,7 +4207,7 @@ private final class GiftViewSheetContent: CombinedComponent { HStack([ AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "≈\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))) ), AnyComponentWithIdentity( id: AnyHashable(1), @@ -5382,6 +5382,12 @@ public class GiftViewScreen: ViewControllerComponentContainer { resellAmounts = uniqueGift.resellAmounts } return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars, nil) + case let .starGiftPurchaseOffer(gift, _, _, _, _), let .starGiftPurchaseOfferDeclined(gift, _, _): + if case let .unique(gift) = gift { + return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil) + } else { + return nil + } default: return nil } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift index eb9e874dd7..8d128e4a0a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift @@ -53,10 +53,12 @@ final class TableComponent: CombinedComponent { private let theme: PresentationTheme private let items: [Item] - - public init(theme: PresentationTheme, items: [Item]) { + private let semiTransparent: Bool + + public init(theme: PresentationTheme, items: [Item], semiTransparent: Bool = false) { self.theme = theme self.items = items + self.semiTransparent = semiTransparent } public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { @@ -66,6 +68,9 @@ final class TableComponent: CombinedComponent { if lhs.items != rhs.items { return false } + if lhs.semiTransparent != rhs.semiTransparent { + return false + } return true } @@ -95,7 +100,10 @@ final class TableComponent: CombinedComponent { let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) - let secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor + var secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor + if context.component.semiTransparent { + secondaryBackgroundColor = borderColor.withMultipliedAlpha(0.5) + } var leftColumnWidth: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 814fcb4edd..448ecd6c1b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -68,6 +68,8 @@ swift_library( "//submodules/TelegramUI/Components/MediaAssetsContext", "//submodules/CheckNode", "//submodules/TelegramNotices", + "//submodules/TelegramUI/Components/AttachmentFileController", + "//submodules/SaveToCameraRoll", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 71d2a6fedd..1b649ed216 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -50,6 +50,8 @@ import UIKitRuntimeUtils import ImageObjectSeparation import SaveProgressScreen import TelegramNotices +import AttachmentFileController +import SaveToCameraRoll private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -3732,7 +3734,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } else if case let .gift(gift) = subject { isGift = true - let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false))] + let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false))] let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages = .single([message]) } else { @@ -5107,8 +5109,65 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } controller.push(locationController) } - + func presentAudioPicker() { + guard let controller = self.controller else { + return + } + + let audioController = storyAudioPickerController( + context: self.context, + selectFromFiles: { [weak self] in + self?.presentAudioFilePicker() + }, + dismissed: { [weak self] in + if let self { + self.mediaEditor?.play() + } + }, + completion: { [weak self] file in + guard let self else { + return + } + let _ = (fetchMediaData( + context: self.context, + postbox: self.context.account.postbox, + userLocation: .other, + mediaReference: file + ) |> deliverOnMainQueue).start(next: { [weak self] state, _ in + guard let self else { + return + } + if case let .data(data) = state { + let path = data.path + + try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true) + + var originalFileName: String = "audio_\(Int.random(in: 0 ..< .max)).mp3" + if let file = file.media as? TelegramMediaFile { + originalFileName = file.fileName ?? "\(file.fileId.id).mp3" + } + + let fileName = "audio_\(originalFileName)" + let copyPath = fullDraftPath(peerId: self.context.account.peerId, path: fileName) + + try? FileManager.default.removeItem(atPath: copyPath) + do { + try FileManager.default.copyItem(atPath: path, toPath: copyPath) + } catch let e { + Logger.shared.log("MediaEditor", "copy file error \(e)") + return + } + + self.insertAudio(path: copyPath, fileName: fileName) + } + }) + } + ) + controller.push(audioController) + } + + func presentAudioFilePicker() { var isSettingTrack = false self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3", "public.mpeg-4-audio", "public.aac-audio", "org.xiph.flac"], forceDarkTheme: true, dismissed: { [weak self] in if let self { @@ -5119,7 +5178,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } }, completion: { [weak self] urls in - guard let self, let mediaEditor = self.mediaEditor, !urls.isEmpty, let url = urls.first else { + guard let self, !urls.isEmpty, let url = urls.first else { return } isSettingTrack = true @@ -5132,7 +5191,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let coordinator = NSFileCoordinator(filePresenter: nil) var error: NSError? coordinator.coordinate(readingItemAt: url, options: .forUploading, error: &error, byAccessor: { sourceUrl in - let fileName = "audio_\(sourceUrl.lastPathComponent)" + let fileName = "audio_\(sourceUrl.lastPathComponent)" let copyPath = fullDraftPath(peerId: self.context.account.peerId, path: fileName) try? FileManager.default.removeItem(atPath: copyPath) @@ -5147,92 +5206,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } Queue.mainQueue().async { - let audioAsset = AVURLAsset(url: URL(fileURLWithPath: copyPath)) - - func loadValues(asset: AVAsset, retryCount: Int, completion: @escaping () -> Void) { - asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: { - if asset.statusOfValue(forKey: "tracks", error: nil) == .loading { - if retryCount < 2 { - Queue.mainQueue().after(0.1, { - loadValues(asset: asset, retryCount: retryCount + 1, completion: completion) - }) - } else { - completion() - } - } else { - completion() - } - }) - } - - loadValues(asset: audioAsset, retryCount: 0, completion: { - var audioDuration: Double = 0.0 - guard let track = audioAsset.tracks(withMediaType: .audio).first else { - Logger.shared.log("MediaEditor", "track is nil") - if isScopedResource { - url.stopAccessingSecurityScopedResource() - } - return - } - - audioDuration = track.timeRange.duration.seconds - if audioDuration.isZero { - Logger.shared.log("MediaEditor", "duration is zero") - if isScopedResource { - url.stopAccessingSecurityScopedResource() - } - return - } - - func maybeFixMisencodedText(_ text: String) -> String { - let charactersToSearchFor = CharacterSet(charactersIn: "àåèîóûþÿ") - if text.lowercased().rangeOfCharacter(from: charactersToSearchFor) != nil { - if let data = text.data(using: .windowsCP1252), let string = String(data: data, encoding: .windowsCP1251) { - return string - } else { - return text - } - } else { - return text - } - } - - var artist: String? - var title: String? - for data in audioAsset.commonMetadata { - if data.commonKey == .commonKeyArtist, let value = data.stringValue { - artist = maybeFixMisencodedText(value) - } - if data.commonKey == .commonKeyTitle, let value = data.stringValue { - title = maybeFixMisencodedText(value) - } - } - - Queue.mainQueue().async { - var audioTrimRange: Range? - var audioOffset: Double? - - if let videoDuration = mediaEditor.originalCappedDuration { - if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound { - audioOffset = -videoStart - } else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound { - audioOffset = -videoStart - } - audioTrimRange = 0 ..< min(videoDuration, audioDuration) - } else { - audioTrimRange = 0 ..< min(15, audioDuration) - } - - mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset) - - mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true) - - self.requestUpdate(transition: .easeInOut(duration: 0.2)) - if isScopedResource { - url.stopAccessingSecurityScopedResource() - } - - mediaEditor.play() + self.insertAudio(path: copyPath, fileName: fileName, dispose: { + if isScopedResource { + url.stopAccessingSecurityScopedResource() } }) } @@ -5244,6 +5220,94 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }), in: .window(.root)) } + private func insertAudio(path: String, fileName: String, dispose: (() -> Void)? = nil) { + guard let mediaEditor = self.mediaEditor else { + return + } + let audioAsset = AVURLAsset(url: URL(fileURLWithPath: path)) + + func loadValues(asset: AVAsset, retryCount: Int, completion: @escaping () -> Void) { + asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: { + if asset.statusOfValue(forKey: "tracks", error: nil) == .loading { + if retryCount < 2 { + Queue.mainQueue().after(0.1, { + loadValues(asset: asset, retryCount: retryCount + 1, completion: completion) + }) + } else { + completion() + } + } else { + completion() + } + }) + } + + loadValues(asset: audioAsset, retryCount: 0, completion: { + var audioDuration: Double = 0.0 + guard let track = audioAsset.tracks(withMediaType: .audio).first else { + Logger.shared.log("MediaEditor", "track is nil") + dispose?() + return + } + + audioDuration = track.timeRange.duration.seconds + if audioDuration.isZero { + Logger.shared.log("MediaEditor", "duration is zero") + dispose?() + return + } + + func maybeFixMisencodedText(_ text: String) -> String { + let charactersToSearchFor = CharacterSet(charactersIn: "àåèîóûþÿ") + if text.lowercased().rangeOfCharacter(from: charactersToSearchFor) != nil { + if let data = text.data(using: .windowsCP1252), let string = String(data: data, encoding: .windowsCP1251) { + return string + } else { + return text + } + } else { + return text + } + } + + var artist: String? + var title: String? + for data in audioAsset.commonMetadata { + if data.commonKey == .commonKeyArtist, let value = data.stringValue { + artist = maybeFixMisencodedText(value) + } + if data.commonKey == .commonKeyTitle, let value = data.stringValue { + title = maybeFixMisencodedText(value) + } + } + + Queue.mainQueue().async { + var audioTrimRange: Range? + var audioOffset: Double? + + if let videoDuration = mediaEditor.originalCappedDuration { + if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound { + audioOffset = -videoStart + } else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound { + audioOffset = -videoStart + } + audioTrimRange = 0 ..< min(videoDuration, audioDuration) + } else { + audioTrimRange = 0 ..< min(15, audioDuration) + } + + mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset) + + mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true) + + self.requestUpdate(transition: .easeInOut(duration: 0.2)) + dispose?() + + mediaEditor.play() + } + }) + } + func presentVideoRemoveConfirmation() { guard let controller = self.controller else { return diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift index 2ee1e33879..be77701a7b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift @@ -348,7 +348,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { var contentSectionItems: [AnyComponentWithIdentity] = [] let usdRate = Double(component.usdWithdrawRate) / 1000.0 / 100.0 - let price = self.starCount == 0 ? "" : "≈\(formatTonUsdValue(Int64(self.starCount), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" + let price = self.starCount == 0 ? "" : "~\(formatTonUsdValue(Int64(self.starCount), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" contentSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( itemGenerator: MessagePriceItem( diff --git a/submodules/TelegramUI/Components/SegmentControlComponent/BUILD b/submodules/TelegramUI/Components/SegmentControlComponent/BUILD new file mode 100644 index 0000000000..b4d37a733c --- /dev/null +++ b/submodules/TelegramUI/Components/SegmentControlComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SegmentControlComponent", + module_name = "SegmentControlComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/SegmentedControlNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift b/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift similarity index 92% rename from submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift rename to submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift index 4f2cb4878b..69d6cb6853 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift +++ b/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift @@ -7,10 +7,15 @@ import ComponentDisplayAdapters import TelegramPresentationData import SegmentedControlNode -final class SegmentControlComponent: Component { - struct Item: Equatable { +public final class SegmentControlComponent: Component { + public struct Item: Equatable { var id: AnyHashable var title: String + + public init(id: AnyHashable, title: String) { + self.id = id + self.title = title + } } let theme: PresentationTheme @@ -18,7 +23,7 @@ final class SegmentControlComponent: Component { let selectedId: AnyHashable? let action: (AnyHashable) -> Void - init( + public init( theme: PresentationTheme, items: [Item], selectedId: AnyHashable?, @@ -30,7 +35,7 @@ final class SegmentControlComponent: Component { self.action = action } - static func ==(lhs: SegmentControlComponent, rhs: SegmentControlComponent) -> Bool { + public static func ==(lhs: SegmentControlComponent, rhs: SegmentControlComponent) -> Bool { if lhs.theme !== rhs.theme { return false } @@ -80,7 +85,7 @@ final class SegmentControlComponent: Component { } } - class View: UIView { + public class View: UIView { private let title = ComponentView() private var component: SegmentControlComponent? @@ -88,7 +93,7 @@ final class SegmentControlComponent: Component { private var nativeSegmentedView: SegmentedControlView? private var legacySegmentedNode: SegmentedControlNode? - override init(frame: CGRect) { + public override init(frame: CGRect) { super.init(frame: frame) } @@ -171,11 +176,11 @@ final class SegmentControlComponent: Component { } } - func makeView() -> View { + public func makeView() -> View { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index af4d150393..c3e484a85e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -220,7 +220,7 @@ final class StarsBalanceComponent: Component { let subtitleText: String if let rate = component.rate { - subtitleText = "≈\(formatTonUsdValue(component.count.value, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))" + subtitleText = "~\(formatTonUsdValue(component.count.value, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))" } else { subtitleText = component.strings.Stars_Intro_YourBalance } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift index 3551b536e3..9130474ac6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift @@ -114,7 +114,7 @@ final class StarsOverviewItemComponent: Component { transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: "≈\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + text: .plain(NSAttributedString(string: "~\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) ) ), environment: {}, diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD index 573462e416..4e7000f275 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index a1ebcb086f..aaa02178ac 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -27,6 +27,7 @@ import TabSelectorComponent import PresentationDataUtils import BalanceNeededScreen import GlassBarButtonComponent +import GlassBackgroundComponent private let amountTag = GenericComponentViewTag() @@ -68,6 +69,7 @@ private final class SheetContent: CombinedComponent { let balanceTitle = Child(MultilineTextComponent.self) let balanceValue = Child(MultilineTextComponent.self) let balanceIcon = Child(BundleIconComponent.self) + let durationPicker = Child(MenuComponent.self) let body: (CombinedComponentContext) -> CGSize = { (context: CombinedComponentContext) -> CGSize in let environment = context.environment[EnvironmentType.self] @@ -356,6 +358,7 @@ private final class SheetContent: CombinedComponent { tonTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestTon } case .starGiftOffer: + //TODO:localize displayCurrencySelector = true starsTitle = "Offer Stars" tonTitle = "Offer TON" @@ -625,6 +628,7 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width - amountAdditionalLabel.size.width / 2.0 - sideInset - 16.0, y: contentSize.height - amountAdditionalLabel.size.height / 2.0))) } + var durationFrame = CGRect() if case .starGiftResell = component.mode { contentSize.height += 24.0 @@ -808,6 +812,14 @@ private final class SheetContent: CombinedComponent { maximumNumberOfLines: 0 )) + let hours = state.duration / 3600 + let durationString: String + if hours == 1 { + durationString = "1 Hour" + } else { + durationString = "\(hours) Hours" + } + let periodSection = periodSection.update( component: ListSectionComponent( theme: theme, @@ -831,15 +843,19 @@ private final class SheetContent: CombinedComponent { ], alignment: .left, spacing: 2.0)), icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "48 Hours", + string: durationString, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemSecondaryTextColor )), maximumNumberOfLines: 1 )))), accessory: .expandArrows, - action: { _ in - + action: { [weak state] _ in + guard let state else { + return + } + state.isPickingDuration = true + state.updated(transition: .easeInOut(duration: 0.25)) } )) )] @@ -853,6 +869,8 @@ private final class SheetContent: CombinedComponent { .clipsToBounds(true) .cornerRadius(10.0) ) + durationFrame = CGRect(origin: CGPoint(x: context.availableSize.width / 2.0 - periodSection.size.width / 2.0, y: contentSize.height), size: periodSection.size) + contentSize.height += periodSection.size.height } @@ -1065,6 +1083,58 @@ private final class SheetContent: CombinedComponent { contentSize.height += buttonInsets.bottom } + + if state.isPickingDuration { + let durationPicker = durationPicker.update( + component: MenuComponent( + theme: theme, + sourceFrame: durationFrame.offsetBy(dx: 0.0, dy: 120.0), + component: AnyComponent(DurationMenuComponent( + theme: theme, + strings: environment.strings, + value: state.duration, + valueUpdated: { [weak state] value in + guard let state else { + return + } + state.isPickingDuration = false + state.duration = value + + state.updated(transition: .easeInOut(duration: 0.25)) + } + )), + dismiss: { [weak state] in + guard let state else { + return + } + state.isPickingDuration = false + state.updated(transition: .easeInOut(duration: 0.25)) + } + ), + availableSize: contentSize, + transition: context.transition + ) + context.add(durationPicker + .position(CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0)) + .appear(ComponentTransition.Appear({ _, view, transition in + if !transition.animation.isImmediate { + if let view = view as? MenuComponent.View { + view.animateIn() + } + } + })) + .disappear(ComponentTransition.Disappear({ view, transition, completion in + if !transition.animation.isImmediate { + if let view = view as? MenuComponent.View { + view.animateOut(completion: completion) + } + } else { + completion() + } + })) + ) + } + return contentSize } @@ -1080,7 +1150,7 @@ private final class SheetContent: CombinedComponent { fileprivate var amount: StarsAmount? fileprivate var currency: CurrencyAmount.Currency = .stars fileprivate var timestamp: Int32? - fileprivate var duration: Int32 = 120 + fileprivate var duration: Int32 = 172800 fileprivate var starsBalance: StarsAmount? private var starsStateDisposable: Disposable? @@ -1091,6 +1161,8 @@ private final class SheetContent: CombinedComponent { var cachedTonImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? + var isPickingDuration = false + init(component: SheetContent) { self.context = component.context self.mode = component.mode @@ -2251,3 +2323,423 @@ private final class CurrencyTabItemComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + + +private final class MenuComponent: Component { + let theme: PresentationTheme + let sourceFrame: CGRect + let component: AnyComponent + let dismiss: () -> Void + + init( + theme: PresentationTheme, + sourceFrame: CGRect, + component: AnyComponent, + dismiss: @escaping () -> Void + ) { + self.theme = theme + self.sourceFrame = sourceFrame + self.component = component + self.dismiss = dismiss + } + + public static func ==(lhs: MenuComponent, rhs: MenuComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.sourceFrame != rhs.sourceFrame { + return false + } + if lhs.component != rhs.component { + return false + } + return true + } + + public final class View: UIView { + private let buttonView: UIButton + private let containerView: GlassBackgroundContainerView + private let backgroundView: GlassBackgroundView + private var componentView: ComponentView? + + private var component: MenuComponent? + + public override init(frame: CGRect) { + self.buttonView = UIButton() + self.containerView = GlassBackgroundContainerView() + self.backgroundView = GlassBackgroundView() + + super.init(frame: frame) + + self.addSubview(self.buttonView) + self.addSubview(self.containerView) + self.containerView.contentView.addSubview(self.backgroundView) + + self.buttonView.addTarget(self, action: #selector(self.tapped), for: .touchUpInside) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func tapped() { + if let component = self.component { + component.dismiss() + } + } + + func animateIn() { + guard let component = self.component else { + return + } + let transition = ComponentTransition.spring(duration: 0.3) + transition.animatePosition(view: self.backgroundView, from: component.sourceFrame.center, to: self.backgroundView.center) + transition.animateScale(view: self.backgroundView, from: 0.2, to: 1.0) + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + + public func animateOut(completion: (() -> Void)? = nil) { + guard let component = self.component else { + return + } + + let transition = ComponentTransition.spring(duration: 0.3) + transition.setPosition(view: self.backgroundView, position: component.sourceFrame.center) + transition.setScale(view: self.backgroundView, scale: 0.2) + self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + completion?() + }) + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.backgroundView.frame.contains(point) && self.buttonView.frame.contains(point) { + return self.buttonView + } + return super.hitTest(point, with: event) + } + + func update(component: MenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + var componentView: ComponentView + var componentTransition = transition + if let current = self.componentView { + componentView = current + } else { + componentTransition = .immediate + componentView = ComponentView() + self.componentView = componentView + } + + let componentSize = componentView.update( + transition: componentTransition, + component: component.component, + environment: {}, + containerSize: availableSize + ) + let backgroundFrame = CGRect(origin: CGPoint(x: component.sourceFrame.maxX - componentSize.width, y: component.sourceFrame.minY - componentSize.height - 20.0), size: componentSize) + if let view = componentView.view { + if view.superview == nil { + self.backgroundView.contentView.addSubview(view) + } + componentTransition.setFrame(view: view, frame: CGRect(origin: .zero, size: componentSize)) + } + + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 30.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) + self.backgroundView.frame = backgroundFrame + + self.containerView.frame = CGRect(origin: .zero, size: availableSize) + self.containerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) + + self.buttonView.frame = CGRect(origin: .zero, size: availableSize) + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class MenuButtonComponent: Component { + let theme: PresentationTheme + let text: String + let isSelected: Bool + let width: CGFloat? + let action: () -> Void + + init( + theme: PresentationTheme, + text: String, + isSelected: Bool, + width: CGFloat?, + action: @escaping () -> Void + ) { + self.theme = theme + self.text = text + self.isSelected = isSelected + self.width = width + self.action = action + } + + static func ==(lhs: MenuButtonComponent, rhs: MenuButtonComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + if lhs.width != rhs.width { + return false + } + return true + } + + final class View: UIView { + private var component: MenuButtonComponent? + private weak var componentState: EmptyComponentState? + + private let selectionLayer = SimpleLayer() + private let title = ComponentView() + private let icon = ComponentView() + private let button = HighlightTrackingButton() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.selectionLayer) + self.selectionLayer.masksToBounds = true + self.selectionLayer.opacity = 0.0 + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + + self.button.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.selectionLayer.opacity = 1.0 + self.selectionLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } else { + self.selectionLayer.opacity = 0.0 + self.selectionLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func buttonPressed() { + if let component = self.component { + component.action() + } + } + + func update(component: MenuButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let leftInset: CGFloat = 60.0 + let rightInset: CGFloat = 40.0 + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + Text(text: component.text, font: Font.regular(17.0), color: component.theme.contextMenu.primaryColor) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: 60.0, y: floorToScreenPixels((availableSize.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let size = CGSize(width: component.width ?? (leftInset + rightInset + titleSize.width), height: availableSize.height) + + if component.isSelected { + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + BundleIconComponent( + name: "Media Gallery/Check", + tintColor: component.theme.contextMenu.primaryColor + ) + ), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: 25.0, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + } + + self.selectionLayer.backgroundColor = component.theme.contextMenu.itemHighlightedBackgroundColor.withMultipliedAlpha(0.5).cgColor + transition.setFrame(layer: self.selectionLayer, frame: CGRect(origin: .zero, size: size).insetBy(dx: 10.0, dy: 0.0)) + self.selectionLayer.cornerRadius = size.height / 2.0 + + if self.button.superview == nil { + self.addSubview(self.button) + } + self.button.frame = CGRect(origin: .zero, size: size) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class DurationMenuComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let value: Int32 + let valueUpdated: (Int32) -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + value: Int32, + valueUpdated: @escaping (Int32) -> Void + ) { + self.theme = theme + self.strings = strings + self.value = value + self.valueUpdated = valueUpdated + } + + public static func ==(lhs: DurationMenuComponent, rhs: DurationMenuComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView: GlassBackgroundView + private var itemViews: [Int32: ComponentView] = [:] + + private var component: DurationMenuComponent? + + private let values: [Int32] = [ + 21600, 43200, 86400, 129600, 172800, 259200 + ] + + private var width: CGFloat? + + public override init(frame: CGRect) { + self.backgroundView = GlassBackgroundView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: DurationMenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let itemHeight: CGFloat = 40.0 + + var maxWidth: CGFloat = 0.0 + var originY: CGFloat = 12.0 + for value in self.values { + let itemView: ComponentView + if let current = self.itemViews[value] { + itemView = current + } else { + itemView = ComponentView() + self.itemViews[value] = itemView + } + + let repeatString: String + let hours = value / 3600 + //TODO:localize + if hours == 1 { + repeatString = "1 hour" + } else { + repeatString = "\(hours) hours" + } + + let itemSize = itemView.update( + transition: transition, + component: AnyComponent( + MenuButtonComponent( + theme: component.theme, + text: repeatString, + isSelected: component.value == value, + width: self.width, + action: { [weak self] in + guard let self else { + return + } + self.component?.valueUpdated(value) + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: itemHeight) + ) + maxWidth = max(maxWidth, itemSize.width) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: originY), size: itemSize) + if let itemView = itemView.view { + if itemView.superview == nil { + self.addSubview(itemView) + } + transition.setFrame(view: itemView, frame: itemFrame) + } + originY += 40.0 + } + + let size = CGSize(width: maxWidth, height: originY + 8.0) + + if self.width == nil { + self.width = maxWidth + Queue.mainQueue().justDispatch { + state.updated() + } + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index dc32b21d4f..096451d9b2 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/BottomButtonPanelComponent", "//submodules/TelegramUI/Components/SliderComponent", + "//submodules/TelegramUI/Components/SegmentControlComponent", "//submodules/CheckNode", "//submodules/Markdown", "//submodules/ContextUI", diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift index 4ceb81cb3c..bac4438def 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift @@ -23,6 +23,7 @@ import TelegramStringFormatting import GalleryData import AnimatedTextComponent import TelegramUIPreferences +import SegmentControlComponent final class DataUsageScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 3bdffbc9e2..a67ef82cbd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1328,6 +1328,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .paidMessagesPriceEdited: self.interfaceInteraction?.openMonoforum() return true + case .starGiftPurchaseOffer: + let controller = self.context.sharedContext.makeGiftViewScreen(context: self.context, message: EngineMessage(message), shareStory: { [weak self] uniqueGift in + Queue.mainQueue().after(0.15) { + if let self { + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: self) + self.push(controller) + } + } + }) + self.push(controller) + return true default: break } @@ -2401,7 +2412,44 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - if let attribute = message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute { + if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(gift, amount, _, isAccepted, isDeclined) = action.action, !isAccepted && !isDeclined { + guard let data = data?.makeData(), message.effectivelyIncoming(strongSelf.context.account.peerId) else { + return + } + if data.count != 1 { + return + } + let buttonType = data.withUnsafeBytes { buffer -> UInt8 in + return buffer.baseAddress!.assumingMemoryBound(to: UInt8.self).pointee + } + + switch buttonType { + case 0: + let _ = strongSelf.context.engine.payments.resolveStarGiftOffer(messageId: message.id, accept: false).startStandalone() + case 1: + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer, case let .unique(gift) = gift else { + return + } + let controller = self.context.sharedContext.makeGiftOfferScreen( + context: self.context, + gift: gift, + peer: peer, + amount: amount, + commit: { [weak self] in + if let self { + let _ = self.context.engine.payments.resolveStarGiftOffer(messageId: message.id, accept: true).startStandalone() + } + } + ) + self.present(controller, in: .window(.root)) + }) + default: + break + } + return + } else if let attribute = message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute { guard let data = data?.makeData() else { return } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index b2b08bdccb..83bfb7699f 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -939,13 +939,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.translationProcessingManager.process = { [weak self, weak context] messageIds in - if let context = context, let translationLang = self?.translationLang { + if let context, let translationLang = self?.translationLang { let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone() } } - self.factCheckProcessingManager.process = { [weak self, weak context] messageIds in - if let context = context, let translationLang = self?.translationLang { - let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone() + self.factCheckProcessingManager.process = { [weak context] messageIds in + if let context { + let _ = context.engine.messages.getMessagesFactCheck(messageIds: Array(messageIds.map(\.messageId))).startStandalone() } } @@ -2746,6 +2746,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto messageIdsToTranslate.append(message.id) } else if let _ = message.media.first(where: { $0 is TelegramMediaPoll }) { messageIdsToTranslate.append(message.id) + } else if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending { + messageIdsToTranslate.append(message.id) } case let .MessageGroupEntry(_, messages, _): for (message, _, _, _, _) in messages { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 0a6d10e448..979044b141 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3860,6 +3860,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftAuctionActiveBidsScreen(context: context) } + public func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController { + return giftOfferAlertController(context: context, gift: gift, peer: peer, amount: amount, commit: commit) + } + public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController { let editorSubject: Signal switch subject { diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index d799a5a87e..f247b69c61 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -172,6 +172,11 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess messageIdsToTranslate.append(messageId) messageIdsSet.insert(messageId) } + } else if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending { + if !messageIdsSet.contains(messageId) { + messageIdsToTranslate.append(messageId) + messageIdsSet.insert(messageId) + } } } else { if !messageIdsSet.contains(messageId) { diff --git a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift index 997a6d3ae0..bde5fba597 100644 --- a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift +++ b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift @@ -17,6 +17,7 @@ import AccountContext import PresentationDataUtils import PremiumPeerShortcutComponent import GiftAnimationComponent +import GlassBarButtonComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -58,7 +59,6 @@ private final class SheetContent: CombinedComponent { } final class State: ComponentState { - var cachedCloseImage: (UIImage, PresentationTheme)? } func makeState() -> State { @@ -68,7 +68,7 @@ private final class SheetContent: CombinedComponent { static var body: Body { let background = Child(RoundedRectangle.self) let animation = Child(GiftAnimationComponent.self) - let closeButton = Child(Button.self) + let closeButton = Child(GlassBarButtonComponent.self) let title = Child(Text.self) let text = Child(BalancedTextComponent.self) @@ -78,7 +78,6 @@ private final class SheetContent: CombinedComponent { return { context in let environment = context.environment[EnvironmentType.self] let component = context.component - let state = context.state let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let theme = presentationData.theme @@ -108,25 +107,27 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 12.0)) ) - let closeImage: UIImage - if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme { - closeImage = image - } else { - closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! - state.cachedCloseImage = (closeImage, theme) - } let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Image(image: closeImage)), - action: { + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { _ in component.dismiss() } ), - availableSize: CGSize(width: 30.0, height: 30.0), + availableSize: CGSize(width: 40.0, height: 40.0), transition: .immediate ) context.add(closeButton - .position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0)) + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 @@ -200,10 +201,10 @@ private final class SheetContent: CombinedComponent { let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0 ), content: AnyComponentWithIdentity( id: AnyHashable(0), @@ -216,7 +217,7 @@ private final class SheetContent: CombinedComponent { controller?.dismissAnimated() } ), - availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 50), + availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0), transition: .immediate ) context.add(button @@ -298,6 +299,7 @@ private final class WebAppSetEmojiStatusSheetComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), followContentSizeChanges: true, clipsContent: true,