diff --git a/Random.txt b/Random.txt index 9ee04165d0..736f305322 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -c796824aa8245ce7309426caa3c4816024abd71d21f3805988aa953a4e826169 +c796824aa8245ce7309426caa3c4816024abd71d21f3805988aa953a4e826168 diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 597ef1ad02..574ad64059 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15459,6 +15459,8 @@ Error: %8$@"; "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.Expired.Hours_1" = "%@ hours"; +"Notification.StarsGiftOffer.Expired.Hours_any" = "%@ hours"; "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"; @@ -15623,3 +15625,6 @@ Error: %8$@"; "Chat.GiftPurchaseOffer.AcceptConfirmation.BadValue" = "The value of this gift is **%@** higher than the offer."; "Chat.GiftPurchaseOffer.AcceptConfirmation.Confirm" = "Confirm Sale"; + +"Notification.PremiumGift.DaysTitle_1" = "%@ Day Premium"; +"Notification.PremiumGift.DaysTitle_any" = "%@ Days Premium"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 0478f4a0e8..c792ab7fcb 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1409,8 +1409,8 @@ 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>, [StarGift.UniqueGift.Attribute]?) -> Void) -> ViewController func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController - func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController - func makeGiftUpgradeVariantsPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]) -> ViewController + func makeGiftOfferScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController + func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController func makeGiftDemoScreen(context: AccountContext) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController diff --git a/submodules/AttachmentUI/BUILD b/submodules/AttachmentUI/BUILD index d29a2ee478..21040b9100 100644 --- a/submodules/AttachmentUI/BUILD +++ b/submodules/AttachmentUI/BUILD @@ -44,6 +44,8 @@ swift_library( "//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/LiquidLens", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 1be4bd9cc0..5670ec7bc0 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -24,6 +24,8 @@ import LegacyMessageInputPanelInputView import ReactionSelectionNode import TopMessageReactions import GlassBackgroundComponent +import LiquidLens +import TabSelectionRecognizer private let legacyButtonSize = CGSize(width: 88.0, height: 49.0) private let glassButtonSize = CGSize(width: 72.0, height: 62.0) @@ -911,13 +913,21 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? private let containerNode: ASDisplayNode - private var backgroundView: GlassBackgroundView? + private var liquidLensView: LiquidLensView? private let backgroundNode: NavigationBackgroundNode private let scrollNode: ASScrollNode private let separatorNode: ASDisplayNode private let selectionNode: ASImageNode - private var buttonViews: [AnyHashable: ComponentHostView] = [:] + + private var itemsContainer = UIView() + private var itemViews: [AnyHashable: ComponentHostView] = [:] + private var selectedItemsContainer = UIView() + private var selectedItemViews: [AnyHashable: ComponentHostView] = [:] + private var itemSizes: [AnyHashable: CGSize] = [:] + + private var tabSelectionRecognizer: TabSelectionRecognizer? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? private var textInputPanelNode: AttachmentTextInputPanelNode? private var progressNode: LoadingProgressNode? @@ -987,7 +997,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { self.mainButtonNode = MainButtonNode() self.secondaryButtonNode = MainButtonNode() - + super.init() self.addSubnode(self.containerNode) @@ -995,7 +1005,6 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { switch style { case .glass: self.scrollNode.cornerRadius = glassButtonSize.height * 0.5 - self.scrollNode.addSubnode(self.selectionNode) case .legacy: self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.separatorNode) @@ -1419,6 +1428,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { self.view.accessibilityTraits = .tabBar } + func requestLayout(transition: ContainedViewLayoutTransition) { + if let layout = self.validLayout { + let _ = self.update(layout: layout, buttons: self.buttons, isSelecting: self.isSelecting, selectionCount: self.selectionCount, elevateProgress: self.elevateProgress, transition: transition) + } + } + @objc private func mainButtonPressed() { self.onMainButtonPressed() } @@ -1473,6 +1488,88 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } } + private func item(at point: CGPoint) -> AnyHashable? { + let contentOffset = self.scrollNode.view.contentOffset.x + let point = point.offsetBy(dx: contentOffset, dy: 0.0) + var closestItem: (AnyHashable, CGFloat)? + for (id, itemView) in self.itemViews { + if itemView.frame.contains(point) { + return id + } else { + let distance = abs(point.x - itemView.center.x) + if let closestItemValue = closestItem { + if closestItemValue.1 > distance { + closestItem = (id, distance) + } + } else { + closestItem = (id, distance) + } + } + } + return closestItem?.0 + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let liquidLensView = self.liquidLensView else { + return + } + let location = recognizer.location(in: liquidLensView.contentView) + switch recognizer.state { + case .began: + if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { + let startX = itemView.frame.minX - 4.0 + self.selectionGestureState = (startX, startX, itemId) + + self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } + case .changed: + if var selectionGestureState = self.selectionGestureState { + selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self.view).x + if let itemId = self.item(at: location) { + selectionGestureState.itemId = itemId + } + self.selectionGestureState = selectionGestureState + self.requestLayout(transition: .immediate) + } + case .ended, .cancelled: + if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state { + guard let index = self.buttons.firstIndex(where: { AnyHashable($0.key) == selectionGestureState.itemId }) else { + return + } + let button = self.buttons[index] + if self.selectionChanged(button) { + self.selectedIndex = index + if self.buttons.count > 5, let button = self.itemViews[button.key] { + let transition = ComponentTransition.spring(duration: 0.4) + + let scrollView = self.scrollNode.view + let targetRect = button.frame.insetBy(dx: -35.0, dy: 0.0) + + var newBounds = scrollView.bounds + if targetRect.minX < scrollView.bounds.minX { + newBounds.origin.x = targetRect.minX + } + else if targetRect.maxX > scrollView.bounds.maxX { + newBounds.origin.x = targetRect.maxX - scrollView.bounds.width + } + let minX = 0.0 + let maxX = scrollView.contentSize.width - scrollView.bounds.width + newBounds.origin.x = max(minX, min(newBounds.origin.x, maxX)) + + transition.setBounds(view: scrollView, bounds: newBounds) + self.updateItemContainers(contentOffset: newBounds.minX, transition: transition) + } + } + } + self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } + default: + break + } + } + func updateViews(transition: ComponentTransition) { guard let layout = self.validLayout else { return @@ -1486,18 +1583,16 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { case .legacy: panelSideInset = 3.0 } - - let visibleRect = self.scrollNode.bounds.insetBy(dx: -180.0, dy: 0.0) - + var distanceBetweenNodes = floorToScreenPixels((width - panelSideInset * 2.0 - self.buttonSize.width) / CGFloat(max(1, self.buttons.count - 1))) let internalWidth = distanceBetweenNodes * CGFloat(self.buttons.count - 1) var buttonWidth = self.buttonSize.width var leftNodeOriginX: CGFloat - var maxButtonsToFit = 6 + var maxButtonsToFit = 5 switch self.panelStyle { case .glass: leftNodeOriginX = layout.safeInsets.left + 3.0 + buttonWidth / 2.0 - if layout.size.width < 400.0 { + if layout.size.width < 420.0 { maxButtonsToFit = 5 } case .legacy: @@ -1521,25 +1616,28 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { var selectionFrame = CGRect() var mostRightX = 0.0 for i in 0 ..< self.buttons.count { - let originX = floor(leftNodeOriginX + CGFloat(i) * distanceBetweenNodes - buttonWidth / 2.0) + let originX = floorToScreenPixels(leftNodeOriginX + CGFloat(i) * distanceBetweenNodes - buttonWidth / 2.0) let buttonFrame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: CGSize(width: buttonWidth, height: self.buttonSize.height)) mostRightX = buttonFrame.maxX - if !visibleRect.intersects(buttonFrame) { - continue - } let type = self.buttons[i] let _ = validIds.insert(type.key) var buttonTransition = transition let buttonView: ComponentHostView - if let current = self.buttonViews[type.key] { + let selectedButtonView: ComponentHostView + if let current = self.itemViews[type.key], let currentSelected = self.selectedItemViews[type.key] { buttonView = current + selectedButtonView = currentSelected } else { buttonTransition = .immediate buttonView = ComponentHostView() - self.buttonViews[type.key] = buttonView - self.scrollNode.view.addSubview(buttonView) + self.itemViews[type.key] = buttonView + self.itemsContainer.addSubview(buttonView) + + selectedButtonView = ComponentHostView() + self.selectedItemViews[type.key] = selectedButtonView + self.selectedItemsContainer.addSubview(selectedButtonView) } if case let .app(bot) = type { @@ -1578,7 +1676,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { style: self.panelStyle == .glass ? .glass : .legacy, type: type, isFirstOrLast: i == 0 || i == self.buttons.count - 1, - isSelected: i == self.selectedIndex, + isSelected: false, strings: self.presentationData.strings, theme: self.presentationData.theme, action: { [weak self] in @@ -1587,7 +1685,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { strongSelf.selectedIndex = i strongSelf.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring))) - if strongSelf.buttons.count > 5, let button = strongSelf.buttonViews[type.key] { + if strongSelf.buttons.count > 5, let button = strongSelf.itemViews[type.key] { strongSelf.scrollNode.view.scrollRectToVisible(button.frame.insetBy(dx: -35.0, dy: 0.0), animated: true) } } @@ -1601,11 +1699,32 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { environment: {}, containerSize: CGSize(width: buttonWidth, height: self.buttonSize.height) ) + self.itemSizes[type.key] = actualButtonSize + + let _ = selectedButtonView.update( + transition: buttonTransition, + component: AnyComponent(AttachButtonComponent( + context: self.context, + style: self.panelStyle == .glass ? .glass : .legacy, + type: type, + isFirstOrLast: i == 0 || i == self.buttons.count - 1, + isSelected: true, + strings: self.presentationData.strings, + theme: self.presentationData.theme, + action: { + }, + longPressAction: { + }) + ), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: self.buttonSize.height) + ) if i == self.selectedIndex { selectionFrame = CGRect(origin: CGPoint(x: buttonFrame.midX - actualButtonSize.width * 0.5, y: buttonFrame.minY), size: actualButtonSize) } buttonTransition.setFrame(view: buttonView, frame: buttonFrame) + buttonTransition.setFrame(view: selectedButtonView, frame: buttonFrame) var accessibilityTitle = "" switch type { case .gallery: @@ -1634,14 +1753,14 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { buttonView.accessibilityTraits = [.button] } var removeIds: [AnyHashable] = [] - for (id, itemView) in self.buttonViews { + for (id, itemView) in self.itemViews { if !validIds.contains(id) { removeIds.append(id) itemView.removeFromSuperview() } } for id in removeIds { - self.buttonViews.removeValue(forKey: id) + self.itemViews.removeValue(forKey: id) } selectionFrame = selectionFrame.insetBy(dx: 1.0, dy: 4.0) @@ -1916,31 +2035,54 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { textPanelWidth = layout.size.width - panelSideInset * 2.0 } + self.updateViews(transition: .immediate) + + var selectionFrame = CGRect() + if self.selectedIndex >= 0 && self.selectedIndex < self.buttons.count, let itemView = self.itemViews[self.buttons[self.selectedIndex].key], let itemSize = self.itemSizes[self.buttons[self.selectedIndex].key] { + let contentOffset = self.scrollNode.view.contentOffset.x + selectionFrame = CGRect(origin: CGPoint(x: itemView.center.x - itemSize.width / 2.0 - contentOffset, y: itemView.frame.minY), size: itemSize).insetBy(dx: -3.0, dy: 0.0) + } + + let lensSelection: (x: CGFloat, width: CGFloat) + if let selectionGestureState = self.selectionGestureState { + lensSelection = (selectionGestureState.currentX, selectionFrame.width) + } else { + lensSelection = (selectionFrame.minX, selectionFrame.width) + } + let glassPanelHeight: CGFloat = 62.0 let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: self.buttonSize.height + insets.bottom)) if case .glass = self.panelStyle { - let backgroundView: GlassBackgroundView - if let current = self.backgroundView { - backgroundView = current + let liquidLensView: LiquidLensView + if let current = self.liquidLensView { + liquidLensView = current } else { - backgroundView = GlassBackgroundView() - self.containerNode.view.addSubview(backgroundView) - self.containerNode.view.addSubview(self.scrollNode.view) - self.backgroundView = backgroundView + liquidLensView = LiquidLensView(useBackgroundContainer: false) + self.containerNode.view.addSubview(liquidLensView) + //self.containerNode.view.addSubview(self.scrollNode.view) + self.liquidLensView = liquidLensView + + liquidLensView.contentView.addSubview(self.itemsContainer) + liquidLensView.selectedContentView.addSubview(self.selectedItemsContainer) + + self.itemsContainer.clipsToBounds = true + self.itemsContainer.layer.cornerRadius = glassPanelHeight * 0.5 + self.selectedItemsContainer.clipsToBounds = true + self.selectedItemsContainer.layer.cornerRadius = glassPanelHeight * 0.5 + + let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + self.tabSelectionRecognizer = tabSelectionRecognizer + liquidLensView.addGestureRecognizer(tabSelectionRecognizer) } let panelSize = CGSize(width: isSelecting ? textPanelWidth : layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0, height: isSelecting ? textPanelHeight - 11.0 : glassPanelHeight) - let backgroundViewColor: UIColor - if self.presentationData.theme.overallDarkAppearance { - backgroundViewColor = self.presentationData.theme.list.modalBlocksBackgroundColor.withAlphaComponent(0.55) - } else { - backgroundViewColor = self.presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.75) - } - let backgroundOriginX: CGFloat = isSelecting ? panelSideInset : floorToScreenPixels((layout.size.width - panelSize.width) / 2.0) - backgroundView.update(size: panelSize, cornerRadius: isSelecting ? 20.0 : glassPanelHeight * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .custom, color: backgroundViewColor), transition: ComponentTransition(transition)) - transition.updatePosition(layer: backgroundView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5)) - transition.updateBounds(layer: backgroundView.layer, bounds: CGRect(origin: .zero, size: panelSize)) + liquidLensView.update(size: panelSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition)) + + transition.updatePosition(layer: liquidLensView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5)) + transition.updateBounds(layer: liquidLensView.layer, bounds: CGRect(origin: .zero, size: panelSize)) + transition.updateFrame(view: self.itemsContainer, frame: CGRect(origin: .zero, size: panelSize)) + transition.updateFrame(view: self.selectedItemsContainer, frame: CGRect(origin: .zero, size: panelSize)) } var containerTransition: ContainedViewLayoutTransition @@ -2002,8 +2144,15 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { let alphaTransition = ContainedViewLayoutTransition.animated(duration: isSelecting ? 0.1 : 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.scrollNode, alpha: isSelecting || isAnyButtonVisible ? 0.0 : 1.0) containerTransition.updateTransformScale(node: self.scrollNode, scale: isSelecting || isAnyButtonVisible ? 0.85 : 1.0) - if let backgroundView = self.backgroundView { - containerTransition.updateTransformScale(layer: backgroundView.layer, scale: isAnyButtonVisible ? 0.85 : 1.0) + + alphaTransition.updateAlpha(layer: self.itemsContainer.layer, alpha: isSelecting || isAnyButtonVisible ? 0.0 : 1.0) + containerTransition.updateTransformScale(layer: self.itemsContainer.layer, scale: isSelecting || isAnyButtonVisible ? 0.85 : 1.0) + + alphaTransition.updateAlpha(layer: self.selectedItemsContainer.layer, alpha: isSelecting || isAnyButtonVisible ? 0.0 : 1.0) + containerTransition.updateTransformScale(layer: self.selectedItemsContainer.layer, scale: isSelecting || isAnyButtonVisible ? 0.85 : 1.0) + + if let liquidLensView = self.liquidLensView { + containerTransition.updateTransformScale(layer: liquidLensView.layer, scale: isAnyButtonVisible ? 0.85 : 1.0) } if isSelectingUpdated { @@ -2022,7 +2171,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { alphaTransition.updateAlpha(node: textInputPanelNode, alpha: 1.0) let componentTransition = ComponentTransition.easeInOut(duration: 0.25) - componentTransition.animateBlur(layer: self.scrollNode.layer, fromRadius: 0.0, toRadius: 10.0) + componentTransition.animateBlur(layer: self.itemsContainer.layer, fromRadius: 0.0, toRadius: 10.0) let blurTransition = ComponentTransition.easeInOut(duration: 0.18) transition.animateTransformScale(node: textInputPanelNode.opaqueActionButtons, from: 0.01) @@ -2054,7 +2203,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { alphaTransition.updateAlpha(node: textInputPanelNode, alpha: 0.0) let componentTransition = ComponentTransition.easeInOut(duration: 0.25) - componentTransition.animateBlur(layer: self.scrollNode.layer, fromRadius: 10.0, toRadius: 0.0) + componentTransition.animateBlur(layer: self.itemsContainer.layer, fromRadius: 10.0, toRadius: 0.0) let blurTransition = ComponentTransition.easeInOut(duration: 0.18) transition.animateTransformScale(layer: textInputPanelNode.opaqueActionButtons.layer, from: CGPoint(x: 1.0, y: 1.0), to: CGPoint(x: 0.01, y: 0.01)) @@ -2086,8 +2235,6 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { transition.updateFrameAsPositionAndBounds(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: self.isSelecting ? panelSideInset - defaultPanelSideInset : panelSideInset, y: self.isSelecting ? -11.0 : 0.0), size: CGSize(width: layout.size.width - panelSideInset * 2.0, height: self.buttonSize.height))) } - self.updateViews(transition: .immediate) - if let progress = self.loadingProgress { let loadingProgressNode: LoadingProgressNode if let current = self.progressNode { @@ -2183,8 +2330,13 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { return containerFrame.height } + func updateItemContainers(contentOffset: CGFloat, transition: ComponentTransition) { + let transform = CATransform3DMakeTranslation(-contentOffset, 0.0, 0.0) + transition.setSublayerTransform(view: self.itemsContainer, transform: transform) + transition.setSublayerTransform(view: self.selectedItemsContainer, transform: transform) + } func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateItemContainers(contentOffset: scrollView.contentOffset.x, transition: .immediate) self.updateViews(transition: .immediate) } } - diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index e031d7675d..bf7844ea91 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -50,6 +50,7 @@ swift_library( "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/Utils/DeviceModel", "//submodules/LegacyMediaPickerUI", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 20a3dbc59a..a3d123cf3a 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -23,6 +23,7 @@ import SaveProgressScreen import DeviceModel import LegacyMediaPickerUI import PassKit +import AlertComponent private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -1258,12 +1259,20 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } var completed = false - let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - if !completed { - completed = true - completionHandler() - } - })]) + + let alertController = AlertScreen( + context: self.context, + title: nil, + text: message, + actions: [ + .init(title: presentationData.strings.Common_OK, type: .default, action: { + if !completed { + completed = true + completionHandler() + } + }) + ] + ) alertController.dismissed = { byOutsideTap in if byOutsideTap { if !completed { @@ -1278,17 +1287,26 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } var completed = false - let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - if !completed { - completed = true - completionHandler(false) - } - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - if !completed { - completed = true - completionHandler(true) - } - })]) + + let alertController = AlertScreen( + context: self.context, + title: nil, + text: message, + actions: [ + .init(title: presentationData.strings.Common_Cancel, action: { + if !completed { + completed = true + completionHandler(false) + } + }), + .init(title: presentationData.strings.Common_OK, type: .default, action: { + if !completed { + completed = true + completionHandler(true) + } + }) + ] + ) alertController.dismissed = { byOutsideTap in if byOutsideTap { if !completed { @@ -1356,17 +1374,25 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU private func presentDownloadConfirmation(fileName: String, proceed: @escaping (Bool) -> Void) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } var completed = false - let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: presentationData.strings.WebBrowser_Download_Confirmation(fileName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - if !completed { - completed = true - proceed(false) - } - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Download_Download, action: { - if !completed { - completed = true - proceed(true) - } - })]) + let alertController = AlertScreen( + context: self.context, + title: nil, + text: presentationData.strings.WebBrowser_Download_Confirmation(fileName).string, + actions: [ + .init(title: presentationData.strings.Common_Cancel, action: { + if !completed { + completed = true + proceed(false) + } + }), + .init(title: presentationData.strings.WebBrowser_Download_Download, type: .default, action: { + if !completed { + completed = true + proceed(true) + } + }) + ] + ) alertController.dismissed = { byOutsideTap in if byOutsideTap { if !completed { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index dae74fd531..ec0a90bdbc 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -4535,7 +4535,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { ) interaction.present(alertController, nil) dismissImpl = { [weak alertController] in - alertController?.dismissAnimated() + alertController?.dismiss() } }, isChannelsTabExpanded: recentItems.isChannelsTabExpanded, diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index 7766272b59..2519f2f6eb 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -43,6 +43,7 @@ public final class MultilineTextWithEntitiesComponent: Component { public let handleSpoilers: Bool public let manualVisibilityControl: Bool public let resetAnimationsOnVisibilityChange: Bool + public let enableLooping: Bool public let displaysAsynchronously: Bool public let maxWidth: CGFloat? public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? @@ -71,6 +72,7 @@ public final class MultilineTextWithEntitiesComponent: Component { handleSpoilers: Bool = false, manualVisibilityControl: Bool = false, resetAnimationsOnVisibilityChange: Bool = false, + enableLooping: Bool = true, displaysAsynchronously: Bool = true, maxWidth: CGFloat? = nil, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, @@ -99,6 +101,7 @@ public final class MultilineTextWithEntitiesComponent: Component { self.handleSpoilers = handleSpoilers self.manualVisibilityControl = manualVisibilityControl self.resetAnimationsOnVisibilityChange = resetAnimationsOnVisibilityChange + self.enableLooping = enableLooping self.displaysAsynchronously = displaysAsynchronously self.maxWidth = maxWidth self.tapAction = tapAction @@ -142,6 +145,9 @@ public final class MultilineTextWithEntitiesComponent: Component { if lhs.resetAnimationsOnVisibilityChange != rhs.resetAnimationsOnVisibilityChange { return false } + if lhs.enableLooping != rhs.enableLooping { + return false + } if lhs.displaysAsynchronously != rhs.displaysAsynchronously { return false } @@ -250,6 +256,7 @@ public final class MultilineTextWithEntitiesComponent: Component { self.textNode.tapAttributeAction = component.tapAction self.textNode.longTapAttributeAction = component.longTapAction self.textNode.spoilerColor = component.spoilerColor + self.textNode.enableLooping = component.enableLooping self.textNode.resetEmojiToFirstFrameAutomatically = component.resetAnimationsOnVisibilityChange diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index 42f1c2b07d..4b11ff0e5d 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -974,8 +974,9 @@ public final class PagerComponent: Component { public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment) diff --git a/submodules/Display/Source/KeyShortcutsController.swift b/submodules/Display/Source/KeyShortcutsController.swift index c8156afb0e..e8afab8996 100644 --- a/submodules/Display/Source/KeyShortcutsController.swift +++ b/submodules/Display/Source/KeyShortcutsController.swift @@ -9,11 +9,7 @@ public class KeyShortcutsController: UIResponder { private var viewControllerEnumerator: (@escaping (ContainableController) -> Bool) -> Void public static var isAvailable: Bool { - if #available(iOSApplicationExtension 8.0, iOS 8.0, *), UIDevice.current.userInterfaceIdiom == .pad { - return true - } else { - return false - } + return true } public init(enumerator: @escaping (@escaping (ContainableController) -> Bool) -> Void) { diff --git a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift index 7ff09939d8..3d65cc98b5 100644 --- a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift @@ -403,8 +403,8 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem } let fieldHeight: CGFloat = 52.0 - let fieldSpacing: CGFloat = 16.0 - let buttonHeight: CGFloat = 50.0 + let fieldSpacing: CGFloat = 10.0 + let buttonHeight: CGFloat = 52.0 let justCreatedCallSeparatorSpacing: CGFloat = 16.0 let justCreatedCallTextSpacing: CGFloat = 45.0 diff --git a/submodules/MediaPickerUI/BUILD b/submodules/MediaPickerUI/BUILD index 3a979c455e..046b14d0ac 100644 --- a/submodules/MediaPickerUI/BUILD +++ b/submodules/MediaPickerUI/BUILD @@ -57,6 +57,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 672868c2f2..7f1b2dbc9c 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -33,6 +33,7 @@ import ComponentFlow import BundleIconComponent import LottieComponent import GlassBarButtonComponent +import AlertComponent final class MediaPickerInteraction { let downloadManager: AssetDownloadManager @@ -2717,16 +2718,22 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } else { text = self.presentationData.strings.Attachment_CancelSelectionAlertText } - - let controller = textAlertController(context: self.context, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertNo, action: { - }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertYes, action: { [weak self] in - self?.dismissAllTooltips() - completion() - })]) - controller.dismissed = { [weak self] _ in + let alertController = AlertScreen( + context: self.context, + title: nil, + text: text, + actions: [ + .init(title: self.presentationData.strings.Attachment_CancelSelectionAlertNo), + .init(title: self.presentationData.strings.Attachment_CancelSelectionAlertYes, type: .default, action: { [weak self] in + self?.dismissAllTooltips() + completion() + }), + ] + ) + alertController.dismissed = { [weak self] _ in self?.isDismissing = false } - self.present(controller, in: .window(.root)) + self.present(alertController, in: .window(.root)) } else { completion() } diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index ac8046b628..c05b9e52c9 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -123,6 +123,8 @@ swift_library( "//submodules/TelegramUI/Components/Premium/PremiumCoinComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/Gifts/TableComponent", + "//submodules/TelegramUI/Components/Gifts/PeerTableCellComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 523309152b..810c269d13 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -493,7 +493,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { } }) case let .starsMore(theme, title): - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { arguments.expandStars() }) case let .starsInfo(_, text): @@ -521,14 +521,14 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { if case let .channel(channel) = peer, case .group = channel.info { isGroup = true } - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text(isGroup ? presentationData.strings.BoostGift_GroupBoosts($0) : presentationData.strings.BoostGift_ChannelsBoosts($0), .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text(isGroup ? presentationData.strings.BoostGift_GroupBoosts($0) : presentationData.strings.BoostGift_ChannelsBoosts($0), .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: { }, setPeerIdWithRevealedOptions: { lhs, rhs in arguments.setItemIdWithRevealedOptions(lhs, rhs) }, removePeer: { id in arguments.removeChannel(id) }) case let .channelAdd(theme, text): - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: { arguments.openChannelsSelection() }) case let .channelsInfo(_, text): @@ -582,7 +582,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { arguments.openPremiumIntro() }) case let .prizeDescription(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateState { state in var updatedState = state updatedState.showPrizeDescription = value @@ -590,7 +590,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { } }) case let .prizeDescriptionText(_, placeholder, value, count): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "\(count)"), text: value, placeholder: placeholder, returnKeyType: .done, spacing: 24.0, maxLength: 128, tag: CreateGiveawayEntryTag.description, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "\(count)"), text: value, placeholder: placeholder, returnKeyType: .done, spacing: 24.0, maxLength: 128, tag: CreateGiveawayEntryTag.description, sectionId: self.section, textUpdated: { value in arguments.updateState { state in var updatedState = state updatedState.prizeDescription = value @@ -616,7 +616,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { } else { text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever } - return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.BoostGift_DateEnds, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.BoostGift_DateEnds, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { arguments.dismissInput() var focus = false arguments.updateState { state in @@ -677,7 +677,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { case let .timeInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .winners(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateState { state in var updatedState = state updatedState.showWinners = value diff --git a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift index ced56dd3a4..a17a139642 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift @@ -23,6 +23,8 @@ import InvisibleInkDustNode import PremiumStarComponent import GlassBarButtonComponent import ButtonComponent +import TableComponent +import PeerTableCellComponent private final class PremiumGiftCodeSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -326,7 +328,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { title: strings.GiftLink_From, component: AnyComponent( Button( - content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)), + content: AnyComponent(PeerTableCellComponent(context: context.component.context, theme: theme, strings: strings, peer: fromPeer)), action: { if let peer = fromPeer, peer.id != accountContext.account.peerId { component.openPeer(peer) @@ -344,7 +346,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { title: strings.GiftLink_To, component: AnyComponent( Button( - content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)), + content: AnyComponent(PeerTableCellComponent(context: context.component.context, theme: theme, strings: strings, peer: toPeer)), action: { if toPeer.id != accountContext.account.peerId { component.openPeer(toPeer) @@ -857,306 +859,6 @@ final class GiftLinkButtonContentComponent: CombinedComponent { } } -private final class TableComponent: CombinedComponent { - class Item: Equatable { - public let id: AnyHashable - public let title: String - public let component: AnyComponent - - public init(id: IdType, title: String, component: AnyComponent) { - self.id = AnyHashable(id) - self.title = title - self.component = component - } - - public static func == (lhs: Item, rhs: Item) -> Bool { - if lhs.id != rhs.id { - return false - } - if lhs.title != rhs.title { - return false - } - if lhs.component != rhs.component { - return false - } - return true - } - } - - private let theme: PresentationTheme - private let items: [Item] - - public init(theme: PresentationTheme, items: [Item]) { - self.theme = theme - self.items = items - } - - public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.items != rhs.items { - return false - } - return true - } - - final class State: ComponentState { - var cachedBorderImage: (UIImage, PresentationTheme)? - } - - func makeState() -> State { - return State() - } - - public static var body: Body { - let leftColumnBackground = Child(Rectangle.self) - let verticalBorder = Child(Rectangle.self) - let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) - let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) - let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) - let outerBorder = Child(Image.self) - - return { context in - let verticalPadding: CGFloat = 11.0 - let horizontalPadding: CGFloat = 12.0 - let borderWidth: CGFloat = 1.0 - - let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor - let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) - - var leftColumnWidth: CGFloat = 0.0 - - var updatedTitleChildren: [_UpdatedChildComponent] = [] - var updatedValueChildren: [_UpdatedChildComponent] = [] - var updatedBorderChildren: [_UpdatedChildComponent] = [] - - for item in context.component.items { - let titleChild = titleChildren[item.id].update( - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: item.title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) - )), - availableSize: context.availableSize, - transition: context.transition - ) - updatedTitleChildren.append(titleChild) - - if titleChild.size.width > leftColumnWidth { - leftColumnWidth = titleChild.size.width - } - } - - leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0) - let rightColumnWidth = context.availableSize.width - leftColumnWidth - - var i = 0 - var rowHeights: [Int: CGFloat] = [:] - var totalHeight: CGFloat = 0.0 - - for item in context.component.items { - let titleChild = updatedTitleChildren[i] - let valueChild = valueChildren[item.id].update( - component: item.component, - availableSize: CGSize(width: rightColumnWidth - horizontalPadding * 2.0, height: context.availableSize.height), - transition: context.transition - ) - updatedValueChildren.append(valueChild) - - let rowHeight = max(40.0, max(titleChild.size.height, valueChild.size.height) + verticalPadding * 2.0) - rowHeights[i] = rowHeight - totalHeight += rowHeight - - if i < context.component.items.count - 1 { - let borderChild = borderChildren[item.id].update( - component: AnyComponent(Rectangle(color: borderColor)), - availableSize: CGSize(width: context.availableSize.width, height: borderWidth), - transition: context.transition - ) - updatedBorderChildren.append(borderChild) - } - - i += 1 - } - - let leftColumnBackground = leftColumnBackground.update( - component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor), - availableSize: CGSize(width: leftColumnWidth, height: totalHeight), - transition: context.transition - ) - context.add( - leftColumnBackground - .position(CGPoint(x: leftColumnWidth / 2.0, y: totalHeight / 2.0)) - ) - - let borderImage: UIImage - if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme { - borderImage = currentImage - } else { - let borderRadius: CGFloat = 14.0 - borderImage = generateImage(CGSize(width: borderRadius * 2.0 + 6.0, height: borderRadius * 2.0 + 6.0), rotatedContext: { size, context in - let bounds = CGRect(origin: .zero, size: size) - context.setFillColor(backgroundColor.cgColor) - context.fill(bounds) - - let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) - context.setBlendMode(.clear) - context.addPath(path) - context.fillPath() - - context.setBlendMode(.normal) - context.setStrokeColor(borderColor.cgColor) - context.setLineWidth(borderWidth) - context.addPath(path) - context.strokePath() - })!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius)) - context.state.cachedBorderImage = (borderImage, context.component.theme) - } - - let outerBorder = outerBorder.update( - component: Image(image: borderImage), - availableSize: CGSize(width: context.availableSize.width, height: totalHeight), - transition: context.transition - ) - context.add(outerBorder - .position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0)) - ) - - let verticalBorder = verticalBorder.update( - component: Rectangle(color: borderColor), - availableSize: CGSize(width: borderWidth, height: totalHeight), - transition: context.transition - ) - context.add( - verticalBorder - .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: totalHeight / 2.0)) - ) - - i = 0 - var originY: CGFloat = 0.0 - for (titleChild, valueChild) in zip(updatedTitleChildren, updatedValueChildren) { - let rowHeight = rowHeights[i] ?? 0.0 - - let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) - let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + horizontalPadding, y: originY + verticalPadding), size: valueChild.size) - - context.add(titleChild - .position(titleFrame.center) - ) - - context.add(valueChild - .position(valueFrame.center) - ) - - if i < updatedBorderChildren.count { - let borderChild = updatedBorderChildren[i] - context.add(borderChild - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0)) - ) - } - - originY += rowHeight - i += 1 - } - - return CGSize(width: context.availableSize.width, height: totalHeight) - } - } -} - -private final class PeerCellComponent: Component { - let context: AccountContext - let textColor: UIColor - let peer: EnginePeer? - - init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) { - self.context = context - self.textColor = textColor - self.peer = peer - } - - static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.textColor !== rhs.textColor { - return false - } - if lhs.peer != rhs.peer { - return false - } - return true - } - - final class View: UIView { - private let avatarNode: AvatarNode - private let text = ComponentView() - - private var component: PeerCellComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) - - super.init(frame: frame) - - self.addSubnode(self.avatarNode) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - self.state = state - - self.avatarNode.setPeer( - context: component.context, - theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, - peer: component.peer, - synchronousLoad: true - ) - - let avatarSize = CGSize(width: 22.0, height: 22.0) - let spacing: CGFloat = 6.0 - - let textSize = self.text.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left)) - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height) - ) - - let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) - - let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) - self.avatarNode.frame = avatarFrame - - if let view = self.text.view { - if view.superview == nil { - self.addSubview(view) - } - let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) - transition.setFrame(view: view, frame: textFrame) - } - - 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 DustComponent: Component { let color: UIColor diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 2ccc8db373..fec0fa3b30 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -1679,34 +1679,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } } }) - - self.newPerksDisposable = combineLatest( - queue: Queue.mainQueue(), - ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessLinksBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessIntroBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessChatbotsBadge(accountManager: context.sharedContext.accountManager) - ).startStrict(next: { [weak self] dismissedBusinessBadge, dismissedBusinessLinksBadge, dismissedBusinessIntroBadge, dismissedBusinessChatbotsBadge in - guard let self else { - return - } - var newPerks: [String] = [] - if !dismissedBusinessBadge { - newPerks.append(PremiumPerk.business.identifier) - } - if !dismissedBusinessLinksBadge { - newPerks.append(PremiumPerk.businessLinks.identifier) - } - if !dismissedBusinessIntroBadge { - newPerks.append(PremiumPerk.businessIntro.identifier) - } - if !dismissedBusinessChatbotsBadge { - newPerks.append(PremiumPerk.businessChatBots.identifier) - } - self.newPerks = newPerks - self.updated() - }) - + self.adsEnabledDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AdsEnabled(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { [weak self] adsEnabled in guard let self else { @@ -2219,7 +2192,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .todo case .business: demoSubject = .business - let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() default: demoSubject = .doubleLimits } @@ -2418,7 +2390,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData)) }) - let _ = ApplicationSpecificNotice.setDismissedBusinessChatbotsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .businessIntro: let _ = (accountContext.sharedContext.makeBusinessIntroSetupScreenInitialData(context: accountContext) |> take(1) @@ -2428,7 +2399,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeBusinessIntroSetupScreen(context: accountContext, initialData: initialData)) }) - let _ = ApplicationSpecificNotice.setDismissedBusinessIntroBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .businessLinks: let _ = (accountContext.sharedContext.makeBusinessLinksSetupScreenInitialData(context: accountContext) |> take(1) @@ -2438,7 +2408,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeBusinessLinksSetupScreen(context: accountContext, initialData: initialData)) }) - let _ = ApplicationSpecificNotice.setDismissedBusinessLinksBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() default: fatalError() } @@ -2457,13 +2426,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .businessAwayMessage case .businessChatBots: demoSubject = .businessChatBots - let _ = ApplicationSpecificNotice.setDismissedBusinessChatbotsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .businessIntro: demoSubject = .businessIntro - let _ = ApplicationSpecificNotice.setDismissedBusinessIntroBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .businessLinks: demoSubject = .businessLinks - let _ = ApplicationSpecificNotice.setDismissedBusinessLinksBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() default: fatalError() } diff --git a/submodules/PresentationDataUtils/BUILD b/submodules/PresentationDataUtils/BUILD index 66b1049608..28dcd03d33 100644 --- a/submodules/PresentationDataUtils/BUILD +++ b/submodules/PresentationDataUtils/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/OverlayStatusController:OverlayStatusController", "//submodules/UrlWhitelist:UrlWhitelist", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PresentationDataUtils/Sources/AlertTheme.swift b/submodules/PresentationDataUtils/Sources/AlertTheme.swift index 1fa7f48d18..12c5d1af70 100644 --- a/submodules/PresentationDataUtils/Sources/AlertTheme.swift +++ b/submodules/PresentationDataUtils/Sources/AlertTheme.swift @@ -4,35 +4,116 @@ import AlertUI import AccountContext import SwiftSignalKit import TelegramPresentationData +import AlertComponent -public func textAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { +public func textAlertController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + forceTheme: PresentationTheme? = nil, + title: String?, + text: String, + actions: [TextAlertAction], + actionLayout: TextAlertContentActionLayout = .horizontal, + allowInputInset: Bool = true, + parseMarkdown: Bool = false, + dismissOnOutsideTap: Bool = true, + linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil +) -> ViewController { return textAlertController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, forceTheme: forceTheme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction) } -public func textAlertController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { +public func textAlertController( + sharedContext: SharedAccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + forceTheme: PresentationTheme? = nil, + title: String?, + text: String, + actions: [TextAlertAction], + actionLayout: TextAlertContentActionLayout = .horizontal, + allowInputInset: Bool = true, + parseMarkdown: Bool = false, + dismissOnOutsideTap: Bool = true, + linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil +) -> ViewController { var presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } - if let forceTheme = forceTheme { + if let forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } - return textAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationData: presentationData), themeSignal: (updatedPresentationData?.signal ?? sharedContext.presentationData) |> map { + let updatedPresentationDataSignal = (updatedPresentationData?.signal ?? sharedContext.presentationData) |> map { presentationData in var presentationData = presentationData if let forceTheme = forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } - return AlertControllerTheme(presentationData: presentationData) - }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction) + return presentationData + } + + let mappedActions: [AlertScreen.Action] = actions.map { action in + let mappedType: AlertScreen.Action.ActionType + switch action.type { + case .genericAction: + mappedType = .generic + case .defaultAction: + mappedType = .default + case .destructiveAction: + mappedType = .destructive + case .defaultDestructiveAction: + mappedType = .defaultDestructive + } + return AlertScreen.Action( + title: action.title, + type: mappedType, + action: action.action + ) + } + + let controller = AlertScreen( + configuration: AlertScreen.Configuration( + actionAlignment: actionLayout == .vertical ? .vertical : .default, + dismissOnOutsideTap: dismissOnOutsideTap, + allowInputInset: allowInputInset + ), + title: title, + text: text, + actions: mappedActions, + updatedPresentationData: (initial: presentationData, signal: updatedPresentationDataSignal) + ) + return controller } -public func textAlertController(sharedContext: SharedAccountContext, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissOnOutsideTap: Bool = true) -> AlertController { +public func textAlertController( + sharedContext: SharedAccountContext, + title: String?, + text: String, + actions: [TextAlertAction], + actionLayout: TextAlertContentActionLayout = .horizontal, + allowInputInset: Bool = true, + dismissOnOutsideTap: Bool = true +) -> AlertController { return textAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationData: sharedContext.currentPresentationData.with { $0 }), themeSignal: sharedContext.presentationData |> map { presentationData in AlertControllerTheme(presentationData: presentationData) }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, dismissOnOutsideTap: dismissOnOutsideTap) } -public func richTextAlertController(context: AccountContext, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissAutomatically: Bool = true) -> AlertController { +public func richTextAlertController( + context: AccountContext, + title: NSAttributedString?, + text: NSAttributedString, + actions: [TextAlertAction], + actionLayout: TextAlertContentActionLayout = .horizontal, + allowInputInset: Bool = true, + dismissAutomatically: Bool = true +) -> AlertController { return richTextAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationData: context.sharedContext.currentPresentationData.with { $0 }), themeSignal: context.sharedContext.presentationData |> map { presentationData in AlertControllerTheme(presentationData: presentationData) }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, dismissAutomatically: dismissAutomatically) } -public func textWithEntitiesAlertController(context: AccountContext, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissAutomatically: Bool = true) -> AlertController { +public func textWithEntitiesAlertController( + context: AccountContext, + title: NSAttributedString?, + text: NSAttributedString, + actions: [TextAlertAction], + actionLayout: TextAlertContentActionLayout = .horizontal, + allowInputInset: Bool = true, + dismissAutomatically: Bool = true +) -> AlertController { return textWithEntitiesAlertController( alertContext: AlertControllerContext( theme: AlertControllerTheme(presentationData: context.sharedContext.currentPresentationData.with { $0 }), diff --git a/submodules/PresentationDataUtils/Sources/OpenUrl.swift b/submodules/PresentationDataUtils/Sources/OpenUrl.swift index 31f70d75d2..1bb1262659 100644 --- a/submodules/PresentationDataUtils/Sources/OpenUrl.swift +++ b/submodules/PresentationDataUtils/Sources/OpenUrl.swift @@ -6,6 +6,7 @@ import AccountContext import OverlayStatusController import UrlWhitelist import TelegramPresentationData +import AlertComponent public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, forceDark: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise? = nil, alertDisplayUpdated: ((ViewController?) -> Void)? = nil) -> Disposable { var concealed = concealed @@ -95,8 +96,10 @@ public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: let alertController = textAlertController(context: context, forceTheme: forceDark ? presentationData.theme : nil, title: nil, text: presentationData.strings.Generic_OpenHiddenLinkAlert(displayUrl).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { disposable.set(openImpl()) })]) - alertController.dismissed = { _ in - alertDisplayUpdated?(nil) + if let alertController = alertController as? AlertScreen { + alertController.dismissed = { _ in + alertDisplayUpdated?(nil) + } } present(alertController) alertDisplayUpdated?(alertController) diff --git a/submodules/QrCodeUI/BUILD b/submodules/QrCodeUI/BUILD index 0308b4224b..e7eb7e221b 100644 --- a/submodules/QrCodeUI/BUILD +++ b/submodules/QrCodeUI/BUILD @@ -31,6 +31,15 @@ swift_library( "//submodules/LegacyComponents:LegacyComponents", "//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI", "//submodules/ImageContentAnalysis:ImageContentAnalysis", + "//submodules/ComponentFlow", + "//submodules/Components/SheetComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 94a293a081..9c975cc28c 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -1,17 +1,23 @@ import Foundation import UIKit -import AsyncDisplayKit import Display +import ComponentFlow import SwiftSignalKit import TelegramCore import TelegramPresentationData -import AppBundle -import QrCode +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import GlassBarButtonComponent +import PlainButtonComponent import AccountContext -import SolidRoundedButtonNode -import AnimatedStickerNode -import TelegramAnimatedStickerNode -import PresentationDataUtils +import Markdown +import TextFormat +import QrCode +import LottieComponent private func shareQrCode(context: AccountContext, link: String, ecl: String, view: UIView) { let _ = (qrCode(string: link, color: .black, backgroundColor: .white, icon: .custom(UIImage(bundleImageName: "Chat/Links/QrLogo")), ecl: ecl) @@ -24,7 +30,7 @@ private func shareQrCode(context: AccountContext, link: String, ecl: String, vie guard let image = image else { return } - + let activityController = UIActivityViewController(activityItems: [image], applicationActivities: nil) if let window = view.window { activityController.popoverPresentationController?.sourceView = window @@ -34,13 +40,336 @@ private func shareQrCode(context: AccountContext, link: String, ecl: String, vie }) } -public final class QrCodeScreen: ViewController { +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: QrCodeScreen.Subject + let dismiss: () -> Void + + init( + context: AccountContext, + subject: QrCodeScreen.Subject, + dismiss: @escaping () -> Void + ) { + self.context = context + self.subject = subject + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + private let idleTimerExtensionDisposable = MetaDisposable() + + private var initialBrightness: CGFloat? + private var brightnessArguments: (Double, Double, CGFloat, CGFloat)? + private var animator: ConstantDisplayLinkAnimator? + + init(context: AccountContext) { + super.init() + + self.idleTimerExtensionDisposable.set(context.sharedContext.applicationBindings.pushIdleTimerExtension()) + + self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateBrightness() + }) + self.animator?.isPaused = true + + self.initialBrightness = UIScreen.main.brightness + self.brightnessArguments = (CACurrentMediaTime(), 0.3, UIScreen.main.brightness, 1.0) + self.updateBrightness() + } + + deinit { + self.idleTimerExtensionDisposable.dispose() + self.animator?.invalidate() + + if UIScreen.main.brightness > 0.99, let initialBrightness = self.initialBrightness { + self.brightnessArguments = (CACurrentMediaTime(), 0.3, UIScreen.main.brightness, initialBrightness) + self.updateBrightness() + } + } + + private func updateBrightness() { + if let (startTime, duration, initial, target) = self.brightnessArguments { + self.animator?.isPaused = false + + let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration))) + let value = initial + (target - initial) * t + + UIScreen.main.brightness = value + + if t >= 1.0 { + self.brightnessArguments = nil + self.animator?.isPaused = true + } + } else { + self.animator?.isPaused = true + } + } + } + + func makeState() -> State { + return State(context: self.context) + } + + static var body: Body { + let qrCode = Child(PlainButtonComponent.self) + let closeButton = Child(GlassBarButtonComponent.self) + let title = Child(Text.self) + let text = Child(BalancedTextComponent.self) + + let button = Child(ButtonComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let controller = environment.controller() + + let theme = environment.theme + let strings = environment.strings + + let link = component.subject.link + let ecl = component.subject.ecl + + let titleString: String + let textString: String + switch component.subject { + case let .invite(_, type): + titleString = strings.InviteLink_QRCode_Title + switch type { + case .group: + textString = strings.InviteLink_QRCode_Info + case .channel: + textString = strings.InviteLink_QRCode_InfoChannel + case .groupCall: + textString = strings.InviteLink_QRCode_InfoGroupCall + } + case .chatFolder: + titleString = strings.InviteLink_QRCodeFolder_Title + textString = strings.InviteLink_QRCodeFolder_Text + default: + titleString = "" + textString = "" + } + + var contentSize = CGSize(width: context.availableSize.width, height: 36.0) + + let closeButton = closeButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { _ in + component.dismiss() + } + ), + availableSize: CGSize(width: 40.0, height: 40.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 + + let title = title.update( + component: Text(text: titleString, font: Font.semibold(17.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height)) + ) + contentSize.height += title.size.height + contentSize.height += 13.0 + + let qrCode = qrCode.update( + component: PlainButtonComponent( + content: AnyComponent(QrCodeComponent(context: component.context, link: link, ecl: ecl)), + action: { [weak controller] in + if let view = controller?.view { + shareQrCode(context: component.context, link: link, ecl: ecl, view: view) + } + }, + animateScale: false + ), + availableSize: CGSize(width: 260.0, height: 260.0), + transition: .immediate + ) + context.add(qrCode + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + qrCode.size.height / 2.0)) + ) + contentSize.height += qrCode.size.height + contentSize.height += 17.0 + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = theme.actionSheet.primaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let text = text.update( + component: BalancedTextComponent( + text: .markdown( + text: textString, + attributes: markdownAttributes + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) + ) + contentSize.height += text.size.height + contentSize.height += 23.0 + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + 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), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.InviteLink_QRCode_Share, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak controller] in + if let view = controller?.view { + shareQrCode(context: component.context, link: link, ecl: ecl, view: view) + } + } + ), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), + transition: .immediate + ) + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + contentSize.height += buttonInsets.bottom + + return contentSize + } + } +} + +private final class QrCodeSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let subject: QrCodeScreen.Subject + + init( + context: AccountContext, + subject: QrCodeScreen.Subject + ) { + self.context = context + self.subject = subject + } + + static func ==(lhs: QrCodeSheetComponent, rhs: QrCodeSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + subject: context.component.subject, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() as? QrCodeScreen { + controller.dismiss(completion: nil) + } + }) + } + )), + style: .glass, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() as? QrCodeScreen { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() as? QrCodeScreen { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class QrCodeScreen: ViewControllerComponentContainer { public enum SubjectType { case group case channel case groupCall } - + public enum Subject { case peer(peer: EnginePeer) case invite(invite: ExportedInvitation, type: SubjectType) @@ -48,471 +377,181 @@ public final class QrCodeScreen: ViewController { var link: String { switch self { - case let .peer(peer): - return "https://t.me/\(peer.addressName ?? "")" - case let .invite(invite, _): - return invite.link ?? "" - case let .chatFolder(slug): - if slug.hasPrefix("https://") { - return slug - } else { - return "https://t.me/addlist/\(slug)" - } + case let .peer(peer): + return "https://t.me/\(peer.addressName ?? "")" + case let .invite(invite, _): + return invite.link ?? "" + case let .chatFolder(slug): + if slug.hasPrefix("https://") { + return slug + } else { + return "https://t.me/addlist/\(slug)" + } } } var ecl: String { switch self { - case .peer: - return "Q" - case .invite: - return "Q" - case .chatFolder: - return "Q" + case .peer: + return "Q" + case .invite: + return "Q" + case .chatFolder: + return "Q" } } } - private var controllerNode: Node { - return self.displayNode as! Node - } - - private var animatedIn = false - private let context: AccountContext - private let subject: QrCodeScreen.Subject - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - private var initialBrightness: CGFloat? - private var brightnessArguments: (Double, Double, CGFloat, CGFloat)? - - private var animator: ConstantDisplayLinkAnimator? - - private let idleTimerExtensionDisposable = MetaDisposable() - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, subject: QrCodeScreen.Subject) { + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + subject: QrCodeScreen.Subject + ) { self.context = context - self.subject = subject - self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + super.init( + context: context, + component: QrCodeSheetComponent( + context: context, + subject: subject + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default // + ) - super.init(navigationBarPresentationData: nil) - - self.statusBar.statusBarStyle = .Ignore - - self.blocksBackgroundWhenInOverlay = true - - self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.presentationData = presentationData - strongSelf.controllerNode.updatePresentationData(presentationData) - } - }) - - self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension()) - - self.statusBar.statusBarStyle = .Ignore - - self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in - self?.updateBrightness() - }) - self.animator?.isPaused = true + self.navigationPresentation = .flatModal } - - required init(coder aDecoder: NSCoder) { + + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - deinit { - self.presentationDataDisposable?.dispose() - self.idleTimerExtensionDisposable.dispose() - self.animator?.invalidate() - } - - override public func loadDisplayNode() { - self.displayNode = Node(context: self.context, presentationData: self.presentationData, subject: self.subject) - self.controllerNode.dismiss = { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - } - self.controllerNode.cancel = { [weak self] in - self?.dismiss() - } - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.animatedIn { - self.animatedIn = true - self.controllerNode.animateIn() - - self.initialBrightness = UIScreen.main.brightness - self.brightnessArguments = (CACurrentMediaTime(), 0.3, UIScreen.main.brightness, 1.0) - self.updateBrightness() - } - } - - private func updateBrightness() { - if let (startTime, duration, initial, target) = self.brightnessArguments { - self.animator?.isPaused = false - - let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration))) - let value = initial + (target - initial) * t - - UIScreen.main.brightness = value - - if t >= 1.0 { - self.brightnessArguments = nil - self.animator?.isPaused = true - } - } else { - self.animator?.isPaused = true - } - } - - override public func dismiss(completion: (() -> Void)? = nil) { - if UIScreen.main.brightness > 0.99, let initialBrightness = self.initialBrightness { - self.brightnessArguments = (CACurrentMediaTime(), 0.3, UIScreen.main.brightness, initialBrightness) - self.updateBrightness() - } - - self.controllerNode.animateOut(completion: completion) - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } - - class Node: ViewControllerTracingNode, ASScrollViewDelegate { - private let context: AccountContext - private let subject: QrCodeScreen.Subject - private var presentationData: PresentationData - - private let dimNode: ASDisplayNode - private let wrappingScrollNode: ASScrollNode - private let contentContainerNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode - private let titleNode: ASTextNode - private let cancelButton: HighlightableButtonNode - - private let textNode: ImmediateTextNode - private let qrButtonNode: HighlightTrackingButtonNode - private let qrImageNode: TransformImageNode - private let qrIconNode: AnimatedStickerNode - private var qrCodeSize: Int? - private let buttonNode: SolidRoundedButtonNode - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - var completion: ((Int32) -> Void)? - var dismiss: (() -> Void)? - var cancel: (() -> Void)? - - init(context: AccountContext, presentationData: PresentationData, subject: QrCodeScreen.Subject) { - self.context = context - self.subject = subject - self.presentationData = presentationData - - self.wrappingScrollNode = ASScrollNode() - self.wrappingScrollNode.view.alwaysBounceVertical = true - self.wrappingScrollNode.view.delaysContentTouches = false - self.wrappingScrollNode.view.canCancelContentTouches = true - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.isOpaque = false - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 - - let backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor - let textColor = self.presentationData.theme.actionSheet.primaryTextColor - let secondaryTextColor = self.presentationData.theme.actionSheet.secondaryTextColor - let accentColor = self.presentationData.theme.actionSheet.controlAccentColor - - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor - - let title: String - let text: String - switch subject { - case let .invite(_, type): - title = self.presentationData.strings.InviteLink_QRCode_Title - switch type { - case .group: - text = self.presentationData.strings.InviteLink_QRCode_Info - case .channel: - text = self.presentationData.strings.InviteLink_QRCode_InfoChannel - case .groupCall: - text = self.presentationData.strings.InviteLink_QRCode_InfoGroupCall - } - case .chatFolder: - title = self.presentationData.strings.InviteLink_QRCodeFolder_Title - text = self.presentationData.strings.InviteLink_QRCodeFolder_Text - default: - title = "" - text = "" - } - - self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor) - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: accentColor, for: .normal) - - self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, isShimmering: false) - - self.textNode = ImmediateTextNode() - self.textNode.maximumNumberOfLines = 3 - self.textNode.textAlignment = .center - - self.qrButtonNode = HighlightTrackingButtonNode() - self.qrImageNode = TransformImageNode() - self.qrImageNode.clipsToBounds = true - self.qrImageNode.cornerRadius = 16.0 - - self.qrIconNode = DefaultAnimatedStickerNodeImpl() - self.qrIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "PlaneLogo"), width: 240, height: 240, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) - self.qrIconNode.visibility = true - - super.init() - - self.backgroundColor = nil - self.isOpaque = false - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - self.addSubnode(self.dimNode) - - self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate - self.addSubnode(self.wrappingScrollNode) - - self.wrappingScrollNode.addSubnode(self.backgroundNode) - self.wrappingScrollNode.addSubnode(self.contentContainerNode) - - self.backgroundNode.addSubnode(self.contentBackgroundNode) - self.contentContainerNode.addSubnode(self.titleNode) - self.contentContainerNode.addSubnode(self.cancelButton) - self.contentContainerNode.addSubnode(self.buttonNode) - - self.contentContainerNode.addSubnode(self.textNode) - self.contentContainerNode.addSubnode(self.qrImageNode) - self.contentContainerNode.addSubnode(self.qrIconNode) - self.contentContainerNode.addSubnode(self.qrButtonNode) - - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: secondaryTextColor) - self.buttonNode.title = self.presentationData.strings.InviteLink_QRCode_Share - - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.buttonNode.pressed = { [weak self] in - if let strongSelf = self{ - shareQrCode(context: strongSelf.context, link: subject.link, ecl: subject.ecl, view: strongSelf.view) - } - } - - self.qrImageNode.setSignal(qrCode(string: subject.link, color: .black, backgroundColor: .white, icon: .cutout, ecl: subject.ecl) |> beforeNext { [weak self] size, _ in - guard let strongSelf = self else { - return - } - strongSelf.qrCodeSize = size - if let (layout, navigationHeight) = strongSelf.containerLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) - } - } |> map { $0.1 }, attemptSynchronously: true) - - self.qrButtonNode.addTarget(self, action: #selector(self.qrPressed), forControlEvents: .touchUpInside) - self.qrButtonNode.highligthedChanged = { [weak self] highlighted in - guard let strongSelf = self else { - return - } - if highlighted { - strongSelf.qrImageNode.alpha = 0.4 - strongSelf.qrIconNode.alpha = 0.4 - } else { - strongSelf.qrImageNode.layer.animateAlpha(from: strongSelf.qrImageNode.alpha, to: 1.0, duration: 0.2) - strongSelf.qrImageNode.alpha = 1.0 - strongSelf.qrIconNode.layer.animateAlpha(from: strongSelf.qrIconNode.alpha, to: 1.0, duration: 0.2) - strongSelf.qrIconNode.alpha = 1.0 - } - } - } - - @objc private func qrPressed() { - self.buttonNode.pressed?() - } - - func updatePresentationData(_ presentationData: PresentationData) { - let previousTheme = self.presentationData.theme - self.presentationData = presentationData - - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor - self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor) - - if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - self.cancelButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) - self.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) - } - - override func didLoad() { - super.didLoad() - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never - } - } - - @objc func cancelButtonPressed() { - self.cancel?() - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.cancelButtonPressed() - } - } - - func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - } - - func animateOut(completion: (() -> Void)? = nil) { - var dimCompleted = false - var offsetCompleted = false - - let internalCompletion: () -> Void = { [weak self] in - if let strongSelf = self, dimCompleted && offsetCompleted { - strongSelf.dismiss?() - } - completion?() - } - - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - dimCompleted = true - internalCompletion() - }) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - offsetCompleted = true - internalCompletion() - }) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { - return self.dimNode.view - } - } - return super.hitTest(point, with: event) - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let contentOffset = scrollView.contentOffset - let additionalTopHeight = max(0.0, -contentOffset.y) - - if additionalTopHeight >= 30.0 { - self.cancelButtonPressed() - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.statusBar, .input]) - insets.top = 32.0 - - let makeImageLayout = self.qrImageNode.asyncLayout() - let imageSide: CGFloat = 240.0 - let imageSize = CGSize(width: imageSide, height: imageSide) - let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) - let _ = imageApply() - - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - let imageFrame = CGRect(origin: CGPoint(x: floor((width - imageSize.width) / 2.0), y: insets.top + 16.0), size: imageSize) - transition.updateFrame(node: self.qrImageNode, frame: imageFrame) - transition.updateFrame(node: self.qrButtonNode, frame: imageFrame) - - if let qrCodeSize = self.qrCodeSize { - let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil) - self.qrIconNode.updateLayout(size: cutoutFrame.size) - transition.updateBounds(node: self.qrIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size)) - transition.updatePosition(node: self.qrIconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0)) - } - - let inset: CGFloat = 32.0 - var textSize = self.textNode.updateLayout(CGSize(width: width - inset * 3.0, height: CGFloat.greatestFiniteMagnitude)) - let textFrame = CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: imageFrame.maxY + 20.0), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) - - var textSpacing: CGFloat = 111.0 - if case .compact = layout.metrics.widthClass, layout.size.width > layout.size.height { - textSize = CGSize() - self.textNode.isHidden = true - textSpacing = 52.0 - } else { - self.textNode.isHidden = false - } - - let buttonSideInset: CGFloat = 16.0 - let bottomInset = insets.bottom + 10.0 - let buttonWidth = layout.size.width - buttonSideInset * 2.0 - let buttonHeight: CGFloat = 50.0 - - let buttonFrame = CGRect(origin: CGPoint(x: floor((width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight)) - transition.updateFrame(node: self.buttonNode, frame: buttonFrame) - let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) - - let titleHeight: CGFloat = 54.0 - let contentHeight = titleHeight + textSize.height + imageSize.height + bottomInset + textSpacing - - 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 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 - } - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight)) - let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 16.0, y: 16.0), size: cancelSize) - transition.updateFrame(node: self.cancelButton, frame: cancelFrame) - - let buttonInset: CGFloat = 16.0 - let doneButtonHeight = self.buttonNode.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - transition.updateFrame(node: self.buttonNode, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: doneButtonHeight)) - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() } } } + +private final class QrCodeComponent: Component { + let context: AccountContext + let link: String + let ecl: String + + init( + context: AccountContext, + link: String, + ecl: String + ) { + self.context = context + self.link = link + self.ecl = ecl + } + + static func ==(lhs: QrCodeComponent, rhs: QrCodeComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.link != rhs.link { + return false + } + if lhs.ecl != rhs.ecl { + return false + } + return true + } + + final class View: UIView { + private var component: QrCodeComponent? + private var state: EmptyComponentState? + + private let imageNode: TransformImageNode + private let icon = ComponentView() + + private var qrCodeSize: Int? + + private var isUpdating = false + + override init(frame: CGRect) { + self.imageNode = TransformImageNode() + + super.init(frame: frame) + + self.backgroundColor = UIColor.white + self.clipsToBounds = true + self.layer.cornerRadius = 24.0 + self.layer.allowsGroupOpacity = true + + self.addSubview(self.imageNode.view) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: QrCodeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + let previousComponent = self.component + self.component = component + self.state = state + + if previousComponent?.link != component.link { + self.imageNode.setSignal(qrCode(string: component.link, color: .black, backgroundColor: .white, icon: .cutout, ecl: component.ecl) |> beforeNext { [weak self] size, _ in + guard let self else { + return + } + self.qrCodeSize = size + if !self.isUpdating { + self.state?.updated() + } + } |> map { $0.1 }, attemptSynchronously: true) + } + + let size = CGSize(width: 256.0, height: 256.0) + let imageSize = CGSize(width: 240.0, height: 240.0) + + let makeImageLayout = self.imageNode.asyncLayout() + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) + let _ = imageApply() + let imageFrame = CGRect(origin: CGPoint(x: (size.width - imageSize.width) / 2.0, y: (size.height - imageSize.height) / 2.0), size: imageSize) + self.imageNode.frame = imageFrame + + if let qrCodeSize = self.qrCodeSize { + let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil) + + let _ = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "PlaneLogo"), + loop: true + )), + environment: {}, + containerSize: cutoutFrame.size + ) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.bounds = CGRect(origin: CGPoint(), size: cutoutFrame.size) + iconView.center = imageFrame.center.offsetBy(dx: 0.0, dy: -1.0) + } + } + + 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) + } +} diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index ffd3f9f0b4..f19a07ca6f 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -131,6 +131,7 @@ swift_library( "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/SliderComponent", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift index df1a0039c9..7cd58def39 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -16,6 +16,7 @@ import AccountUtils import PremiumUI import PasswordSetupUI import StorageUsageScreen +import AlertComponent private struct DeleteAccountOptionsArguments { let changePhoneNumber: () -> Void @@ -102,43 +103,43 @@ private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable { let arguments = arguments as! DeleteAccountOptionsArguments switch self { case let .changePhoneNumber(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.changePhoneNumber() }) case let .addAccount(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteAddAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.deleteAddAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.addAccount() }) case let .changePrivacy(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.security, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.security, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setupPrivacy() }) case let .setTwoStepAuth(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetTwoStepAuth, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.deleteSetTwoStepAuth, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setupTwoStepAuth() }) case let .setPasscode(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.deleteSetPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setPasscode() }) case let .clearCache(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.dataAndStorage, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.dataAndStorage, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.clearCache() }) case let .clearSyncedContacts(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.clearSynced, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.clearSynced, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.clearSyncedContacts() }) case let .deleteChats(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteChats, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.deleteChats, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.deleteChats() }) case let .contactSupport(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.contactSupport() }) case let .deleteAccount(_, title): - return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.deleteAccount() }) } @@ -362,32 +363,36 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) }) } - - let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { - openFaq(resolvedUrlPromise) - dismissImpl?() - }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - supportPeerDisposable.set((supportPeer.get() - |> take(1) - |> deliverOnMainQueue).start(next: { peerId in - guard let peerId = peerId else { - return - } - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer else { + let alertController = AlertScreen( + context: context, + title: nil, + text: presentationData.strings.Settings_FAQ_Intro, + actions: [ + .init(title: presentationData.strings.Settings_FAQ_Button, action: { + openFaq(resolvedUrlPromise) + dismissImpl?() + }), + .init(title: presentationData.strings.Common_OK, type: .default, action: { + supportPeerDisposable.set((supportPeer.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peerId in + guard let peerId else { return } - if let navigationController = navigationController { - dismissImpl?() - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) - } - }) - })) - }) - ]) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + if let navigationController = navigationController { + dismissImpl?() + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + } + }) + })) + }) + ] + ) alertController.dismissed = { _ in addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_support_cancel") } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 09aeb82886..7fe1edbcac 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -1406,10 +1406,10 @@ public func privacyAndSecurityController( let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = textAlertController( context: context, title: emailPattern, text: presentationData.strings.PrivacySettings_LoginEmailAlertText, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.PrivacySettings_LoginEmailAlertChange, action: { + TextAlertAction(type: .defaultAction, title: presentationData.strings.PrivacySettings_LoginEmailAlertChange, action: { setupEmailImpl?(emailPattern) }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) ], actionLayout: .vertical diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 1f0fce870e..7b670a5661 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -417,6 +417,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1279654347] = { return Api.InputMedia.parse_inputMediaPhoto($0) } dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) } dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) } + dict[-207018934] = { return Api.InputMedia.parse_inputMediaStakeDice($0) } dict[-1979852936] = { return Api.InputMedia.parse_inputMediaStory($0) } dict[-1614454818] = { return Api.InputMedia.parse_inputMediaTodo($0) } dict[58495792] = { return Api.InputMedia.parse_inputMediaUploadedDocument($0) } @@ -660,7 +661,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1386050360] = { return Api.MessageExtendedMedia.parse_messageExtendedMediaPreview($0) } dict[1313731771] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) } dict[1882335561] = { return Api.MessageMedia.parse_messageMediaContact($0) } - dict[1065280907] = { return Api.MessageMedia.parse_messageMediaDice($0) } + dict[147581959] = { return Api.MessageMedia.parse_messageMediaDice($0) } dict[1389939929] = { return Api.MessageMedia.parse_messageMediaDocument($0) } dict[1038967584] = { return Api.MessageMedia.parse_messageMediaEmpty($0) } dict[-38694904] = { return Api.MessageMedia.parse_messageMediaGame($0) } @@ -1127,6 +1128,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-302247650] = { return Api.Update.parse_updateDraftMessage($0) } dict[457133559] = { return Api.Update.parse_updateEditChannelMessage($0) } dict[-469536605] = { return Api.Update.parse_updateEditMessage($0) } + dict[-73640838] = { return Api.Update.parse_updateEmojiGameInfo($0) } dict[386986326] = { return Api.Update.parse_updateEncryptedChatTyping($0) } dict[956179895] = { return Api.Update.parse_updateEncryptedMessagesRead($0) } dict[-1264392051] = { return Api.Update.parse_updateEncryption($0) } @@ -1417,6 +1419,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-253500010] = { return Api.messages.Dialogs.parse_dialogsNotModified($0) } dict[1910543603] = { return Api.messages.Dialogs.parse_dialogsSlice($0) } dict[-1506535550] = { return Api.messages.DiscussionMessage.parse_discussionMessage($0) } + dict[1155883043] = { return Api.messages.EmojiGameInfo.parse_emojiGameDiceInfo($0) } + dict[1508266805] = { return Api.messages.EmojiGameInfo.parse_emojiGameUnavailable($0) } + dict[-1585592191] = { return Api.messages.EmojiGameOutcome.parse_emojiGameOutcome($0) } dict[-2011186869] = { return Api.messages.EmojiGroups.parse_emojiGroups($0) } dict[1874111879] = { return Api.messages.EmojiGroups.parse_emojiGroupsNotModified($0) } dict[410107472] = { return Api.messages.ExportedChatInvite.parse_exportedChatInvite($0) } @@ -2573,6 +2578,10 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.DiscussionMessage: _1.serialize(buffer, boxed) + case let _1 as Api.messages.EmojiGameInfo: + _1.serialize(buffer, boxed) + case let _1 as Api.messages.EmojiGameOutcome: + _1.serialize(buffer, boxed) case let _1 as Api.messages.EmojiGroups: _1.serialize(buffer, boxed) case let _1 as Api.messages.ExportedChatInvite: diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 4914c64e63..4d4f58e93b 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -13,6 +13,7 @@ public extension Api { case inputMediaPhoto(flags: Int32, id: Api.InputPhoto, ttlSeconds: Int32?) case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?) + case inputMediaStakeDice(gameHash: String, tonAmount: Int64, clientSeed: Buffer) case inputMediaStory(peer: Api.InputPeer, id: Int32) case inputMediaTodo(todo: Api.TodoList) case inputMediaUploadedDocument(flags: Int32, file: Api.InputFile, thumb: Api.InputFile?, mimeType: String, attributes: [Api.DocumentAttribute], stickers: [Api.InputDocument]?, videoCover: Api.InputPhoto?, videoTimestamp: Int32?, ttlSeconds: Int32?) @@ -148,6 +149,14 @@ public extension Api { item.serialize(buffer, true) }} break + case .inputMediaStakeDice(let gameHash, let tonAmount, let clientSeed): + if boxed { + buffer.appendInt32(-207018934) + } + serializeString(gameHash, buffer: buffer, boxed: false) + serializeInt64(tonAmount, buffer: buffer, boxed: false) + serializeBytes(clientSeed, buffer: buffer, boxed: false) + break case .inputMediaStory(let peer, let id): if boxed { buffer.appendInt32(-1979852936) @@ -245,6 +254,8 @@ public extension Api { return ("inputMediaPhotoExternal", [("flags", flags as Any), ("url", url as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaPoll(let flags, let poll, let correctAnswers, let solution, let solutionEntities): return ("inputMediaPoll", [("flags", flags as Any), ("poll", poll as Any), ("correctAnswers", correctAnswers as Any), ("solution", solution as Any), ("solutionEntities", solutionEntities as Any)]) + case .inputMediaStakeDice(let gameHash, let tonAmount, let clientSeed): + return ("inputMediaStakeDice", [("gameHash", gameHash as Any), ("tonAmount", tonAmount as Any), ("clientSeed", clientSeed as Any)]) case .inputMediaStory(let peer, let id): return ("inputMediaStory", [("peer", peer as Any), ("id", id as Any)]) case .inputMediaTodo(let todo): @@ -533,6 +544,23 @@ public extension Api { return nil } } + public static func parse_inputMediaStakeDice(_ reader: BufferReader) -> InputMedia? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + var _3: Buffer? + _3 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputMedia.inputMediaStakeDice(gameHash: _1!, tonAmount: _2!, clientSeed: _3!) + } + else { + return nil + } + } public static func parse_inputMediaStory(_ reader: BufferReader) -> InputMedia? { var _1: Api.InputPeer? if let signature = reader.readInt32() { @@ -990,113 +1018,3 @@ public extension Api { } } -public extension Api { - enum InputPaymentCredentials: TypeConstructorDescription { - case inputPaymentCredentials(flags: Int32, data: Api.DataJSON) - case inputPaymentCredentialsApplePay(paymentData: Api.DataJSON) - case inputPaymentCredentialsGooglePay(paymentToken: Api.DataJSON) - case inputPaymentCredentialsSaved(id: String, tmpPassword: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputPaymentCredentials(let flags, let data): - if boxed { - buffer.appendInt32(873977640) - } - serializeInt32(flags, buffer: buffer, boxed: false) - data.serialize(buffer, true) - break - case .inputPaymentCredentialsApplePay(let paymentData): - if boxed { - buffer.appendInt32(178373535) - } - paymentData.serialize(buffer, true) - break - case .inputPaymentCredentialsGooglePay(let paymentToken): - if boxed { - buffer.appendInt32(-1966921727) - } - paymentToken.serialize(buffer, true) - break - case .inputPaymentCredentialsSaved(let id, let tmpPassword): - if boxed { - buffer.appendInt32(-1056001329) - } - serializeString(id, buffer: buffer, boxed: false) - serializeBytes(tmpPassword, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputPaymentCredentials(let flags, let data): - return ("inputPaymentCredentials", [("flags", flags as Any), ("data", data as Any)]) - case .inputPaymentCredentialsApplePay(let paymentData): - return ("inputPaymentCredentialsApplePay", [("paymentData", paymentData as Any)]) - case .inputPaymentCredentialsGooglePay(let paymentToken): - return ("inputPaymentCredentialsGooglePay", [("paymentToken", paymentToken as Any)]) - case .inputPaymentCredentialsSaved(let id, let tmpPassword): - return ("inputPaymentCredentialsSaved", [("id", id as Any), ("tmpPassword", tmpPassword as Any)]) - } - } - - public static func parse_inputPaymentCredentials(_ reader: BufferReader) -> InputPaymentCredentials? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.DataJSON? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.DataJSON - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputPaymentCredentials.inputPaymentCredentials(flags: _1!, data: _2!) - } - else { - return nil - } - } - public static func parse_inputPaymentCredentialsApplePay(_ reader: BufferReader) -> InputPaymentCredentials? { - var _1: Api.DataJSON? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.DataJSON - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPaymentCredentials.inputPaymentCredentialsApplePay(paymentData: _1!) - } - else { - return nil - } - } - public static func parse_inputPaymentCredentialsGooglePay(_ reader: BufferReader) -> InputPaymentCredentials? { - var _1: Api.DataJSON? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.DataJSON - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPaymentCredentials.inputPaymentCredentialsGooglePay(paymentToken: _1!) - } - else { - return nil - } - } - public static func parse_inputPaymentCredentialsSaved(_ reader: BufferReader) -> InputPaymentCredentials? { - var _1: String? - _1 = parseString(reader) - var _2: Buffer? - _2 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputPaymentCredentials.inputPaymentCredentialsSaved(id: _1!, tmpPassword: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index ae88c1490f..2a09b85776 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -1,3 +1,113 @@ +public extension Api { + enum InputPaymentCredentials: TypeConstructorDescription { + case inputPaymentCredentials(flags: Int32, data: Api.DataJSON) + case inputPaymentCredentialsApplePay(paymentData: Api.DataJSON) + case inputPaymentCredentialsGooglePay(paymentToken: Api.DataJSON) + case inputPaymentCredentialsSaved(id: String, tmpPassword: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputPaymentCredentials(let flags, let data): + if boxed { + buffer.appendInt32(873977640) + } + serializeInt32(flags, buffer: buffer, boxed: false) + data.serialize(buffer, true) + break + case .inputPaymentCredentialsApplePay(let paymentData): + if boxed { + buffer.appendInt32(178373535) + } + paymentData.serialize(buffer, true) + break + case .inputPaymentCredentialsGooglePay(let paymentToken): + if boxed { + buffer.appendInt32(-1966921727) + } + paymentToken.serialize(buffer, true) + break + case .inputPaymentCredentialsSaved(let id, let tmpPassword): + if boxed { + buffer.appendInt32(-1056001329) + } + serializeString(id, buffer: buffer, boxed: false) + serializeBytes(tmpPassword, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputPaymentCredentials(let flags, let data): + return ("inputPaymentCredentials", [("flags", flags as Any), ("data", data as Any)]) + case .inputPaymentCredentialsApplePay(let paymentData): + return ("inputPaymentCredentialsApplePay", [("paymentData", paymentData as Any)]) + case .inputPaymentCredentialsGooglePay(let paymentToken): + return ("inputPaymentCredentialsGooglePay", [("paymentToken", paymentToken as Any)]) + case .inputPaymentCredentialsSaved(let id, let tmpPassword): + return ("inputPaymentCredentialsSaved", [("id", id as Any), ("tmpPassword", tmpPassword as Any)]) + } + } + + public static func parse_inputPaymentCredentials(_ reader: BufferReader) -> InputPaymentCredentials? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.DataJSON? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputPaymentCredentials.inputPaymentCredentials(flags: _1!, data: _2!) + } + else { + return nil + } + } + public static func parse_inputPaymentCredentialsApplePay(_ reader: BufferReader) -> InputPaymentCredentials? { + var _1: Api.DataJSON? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPaymentCredentials.inputPaymentCredentialsApplePay(paymentData: _1!) + } + else { + return nil + } + } + public static func parse_inputPaymentCredentialsGooglePay(_ reader: BufferReader) -> InputPaymentCredentials? { + var _1: Api.DataJSON? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPaymentCredentials.inputPaymentCredentialsGooglePay(paymentToken: _1!) + } + else { + return nil + } + } + public static func parse_inputPaymentCredentialsSaved(_ reader: BufferReader) -> InputPaymentCredentials? { + var _1: String? + _1 = parseString(reader) + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputPaymentCredentials.inputPaymentCredentialsSaved(id: _1!, tmpPassword: _2!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputPeer: TypeConstructorDescription { case inputPeerChannel(channelId: Int64, accessHash: Int64) diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 90af1af4c3..ccdc5baba2 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -709,7 +709,7 @@ public extension Api { public extension Api { indirect enum MessageMedia: TypeConstructorDescription { case messageMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String, userId: Int64) - case messageMediaDice(value: Int32, emoticon: String) + case messageMediaDice(flags: Int32, value: Int32, emoticon: String, gameOutcome: Api.messages.EmojiGameOutcome?) case messageMediaDocument(flags: Int32, document: Api.Document?, altDocuments: [Api.Document]?, videoCover: Api.Photo?, videoTimestamp: Int32?, ttlSeconds: Int32?) case messageMediaEmpty case messageMediaGame(game: Api.Game) @@ -740,12 +740,14 @@ public extension Api { serializeString(vcard, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false) break - case .messageMediaDice(let value, let emoticon): + case .messageMediaDice(let flags, let value, let emoticon, let gameOutcome): if boxed { - buffer.appendInt32(1065280907) + buffer.appendInt32(147581959) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(value, buffer: buffer, boxed: false) serializeString(emoticon, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {gameOutcome!.serialize(buffer, true)} break case .messageMediaDocument(let flags, let document, let altDocuments, let videoCover, let videoTimestamp, let ttlSeconds): if boxed { @@ -930,8 +932,8 @@ public extension Api { switch self { case .messageMediaContact(let phoneNumber, let firstName, let lastName, let vcard, let userId): return ("messageMediaContact", [("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any), ("userId", userId as Any)]) - case .messageMediaDice(let value, let emoticon): - return ("messageMediaDice", [("value", value as Any), ("emoticon", emoticon as Any)]) + case .messageMediaDice(let flags, let value, let emoticon, let gameOutcome): + return ("messageMediaDice", [("flags", flags as Any), ("value", value as Any), ("emoticon", emoticon as Any), ("gameOutcome", gameOutcome as Any)]) case .messageMediaDocument(let flags, let document, let altDocuments, let videoCover, let videoTimestamp, let ttlSeconds): return ("messageMediaDocument", [("flags", flags as Any), ("document", document as Any), ("altDocuments", altDocuments as Any), ("videoCover", videoCover as Any), ("videoTimestamp", videoTimestamp as Any), ("ttlSeconds", ttlSeconds as Any)]) case .messageMediaEmpty: @@ -995,12 +997,20 @@ public extension Api { public static func parse_messageMediaDice(_ reader: BufferReader) -> MessageMedia? { var _1: Int32? _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: Api.messages.EmojiGameOutcome? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.messages.EmojiGameOutcome + } } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.MessageMedia.messageMediaDice(value: _1!, emoticon: _2!) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MessageMedia.messageMediaDice(flags: _1!, value: _2!, emoticon: _3!, gameOutcome: _4) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 075ddc821d..e9cfd50008 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1247,6 +1247,7 @@ public extension Api { case updateDraftMessage(flags: Int32, peer: Api.Peer, topMsgId: Int32?, savedPeerId: Api.Peer?, draft: Api.DraftMessage) case updateEditChannelMessage(message: Api.Message, pts: Int32, ptsCount: Int32) case updateEditMessage(message: Api.Message, pts: Int32, ptsCount: Int32) + case updateEmojiGameInfo(info: Api.messages.EmojiGameInfo) case updateEncryptedChatTyping(chatId: Int32) case updateEncryptedMessagesRead(chatId: Int32, maxDate: Int32, date: Int32) case updateEncryption(chat: Api.EncryptedChat, date: Int32) @@ -1887,6 +1888,12 @@ public extension Api { serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(ptsCount, buffer: buffer, boxed: false) break + case .updateEmojiGameInfo(let info): + if boxed { + buffer.appendInt32(-73640838) + } + info.serialize(buffer, true) + break case .updateEncryptedChatTyping(let chatId): if boxed { buffer.appendInt32(386986326) @@ -2774,6 +2781,8 @@ public extension Api { return ("updateEditChannelMessage", [("message", message as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateEditMessage(let message, let pts, let ptsCount): return ("updateEditMessage", [("message", message as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) + case .updateEmojiGameInfo(let info): + return ("updateEmojiGameInfo", [("info", info as Any)]) case .updateEncryptedChatTyping(let chatId): return ("updateEncryptedChatTyping", [("chatId", chatId as Any)]) case .updateEncryptedMessagesRead(let chatId, let maxDate, let date): @@ -4126,6 +4135,19 @@ public extension Api { return nil } } + public static func parse_updateEmojiGameInfo(_ reader: BufferReader) -> Update? { + var _1: Api.messages.EmojiGameInfo? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.messages.EmojiGameInfo + } + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateEmojiGameInfo(info: _1!) + } + else { + return nil + } + } public static func parse_updateEncryptedChatTyping(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index 1341629fd0..253d458276 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1060,6 +1060,120 @@ public extension Api.messages { } } +public extension Api.messages { + enum EmojiGameInfo: TypeConstructorDescription { + case emojiGameDiceInfo(flags: Int32, gameHash: String, prevStake: Int64, currentStreak: Int32, params: [Int32], playsLeft: Int32?) + case emojiGameUnavailable + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .emojiGameDiceInfo(let flags, let gameHash, let prevStake, let currentStreak, let params, let playsLeft): + if boxed { + buffer.appendInt32(1155883043) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(gameHash, buffer: buffer, boxed: false) + serializeInt64(prevStake, buffer: buffer, boxed: false) + serializeInt32(currentStreak, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(params.count)) + for item in params { + serializeInt32(item, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(playsLeft!, buffer: buffer, boxed: false)} + break + case .emojiGameUnavailable: + if boxed { + buffer.appendInt32(1508266805) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .emojiGameDiceInfo(let flags, let gameHash, let prevStake, let currentStreak, let params, let playsLeft): + return ("emojiGameDiceInfo", [("flags", flags as Any), ("gameHash", gameHash as Any), ("prevStake", prevStake as Any), ("currentStreak", currentStreak as Any), ("params", params as Any), ("playsLeft", playsLeft as Any)]) + case .emojiGameUnavailable: + return ("emojiGameUnavailable", []) + } + } + + public static func parse_emojiGameDiceInfo(_ reader: BufferReader) -> EmojiGameInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() + var _5: [Int32]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + var _6: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.messages.EmojiGameInfo.emojiGameDiceInfo(flags: _1!, gameHash: _2!, prevStake: _3!, currentStreak: _4!, params: _5!, playsLeft: _6) + } + else { + return nil + } + } + public static func parse_emojiGameUnavailable(_ reader: BufferReader) -> EmojiGameInfo? { + return Api.messages.EmojiGameInfo.emojiGameUnavailable + } + + } +} +public extension Api.messages { + enum EmojiGameOutcome: TypeConstructorDescription { + case emojiGameOutcome(seed: Buffer, tonAmount: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .emojiGameOutcome(let seed, let tonAmount): + if boxed { + buffer.appendInt32(-1585592191) + } + serializeBytes(seed, buffer: buffer, boxed: false) + serializeInt64(tonAmount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .emojiGameOutcome(let seed, let tonAmount): + return ("emojiGameOutcome", [("seed", seed as Any), ("tonAmount", tonAmount as Any)]) + } + } + + public static func parse_emojiGameOutcome(_ reader: BufferReader) -> EmojiGameOutcome? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.EmojiGameOutcome.emojiGameOutcome(seed: _1!, tonAmount: _2!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum EmojiGroups: TypeConstructorDescription { case emojiGroups(hash: Int32, groups: [Api.EmojiGroup]) @@ -1260,155 +1374,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum FavedStickers: TypeConstructorDescription { - case favedStickers(hash: Int64, packs: [Api.StickerPack], stickers: [Api.Document]) - case favedStickersNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .favedStickers(let hash, let packs, let stickers): - if boxed { - buffer.appendInt32(750063767) - } - serializeInt64(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(packs.count)) - for item in packs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stickers.count)) - for item in stickers { - item.serialize(buffer, true) - } - break - case .favedStickersNotModified: - if boxed { - buffer.appendInt32(-1634752813) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .favedStickers(let hash, let packs, let stickers): - return ("favedStickers", [("hash", hash as Any), ("packs", packs as Any), ("stickers", stickers as Any)]) - case .favedStickersNotModified: - return ("favedStickersNotModified", []) - } - } - - public static func parse_favedStickers(_ reader: BufferReader) -> FavedStickers? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.StickerPack]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) - } - var _3: [Api.Document]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.FavedStickers.favedStickers(hash: _1!, packs: _2!, stickers: _3!) - } - else { - return nil - } - } - public static func parse_favedStickersNotModified(_ reader: BufferReader) -> FavedStickers? { - return Api.messages.FavedStickers.favedStickersNotModified - } - - } -} -public extension Api.messages { - enum FeaturedStickers: TypeConstructorDescription { - case featuredStickers(flags: Int32, hash: Int64, count: Int32, sets: [Api.StickerSetCovered], unread: [Int64]) - case featuredStickersNotModified(count: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .featuredStickers(let flags, let hash, let count, let sets, let unread): - if boxed { - buffer.appendInt32(-1103615738) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(hash, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(unread.count)) - for item in unread { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .featuredStickersNotModified(let count): - if boxed { - buffer.appendInt32(-958657434) - } - serializeInt32(count, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .featuredStickers(let flags, let hash, let count, let sets, let unread): - return ("featuredStickers", [("flags", flags as Any), ("hash", hash as Any), ("count", count as Any), ("sets", sets as Any), ("unread", unread as Any)]) - case .featuredStickersNotModified(let count): - return ("featuredStickersNotModified", [("count", count as Any)]) - } - } - - public static func parse_featuredStickers(_ reader: BufferReader) -> FeaturedStickers? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - var _5: [Int64]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.FeaturedStickers.featuredStickers(flags: _1!, hash: _2!, count: _3!, sets: _4!, unread: _5!) - } - else { - return nil - } - } - public static func parse_featuredStickersNotModified(_ reader: BufferReader) -> FeaturedStickers? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.FeaturedStickers.featuredStickersNotModified(count: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index a0c446bb46..489cfe7c9d 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -1,3 +1,155 @@ +public extension Api.messages { + enum FavedStickers: TypeConstructorDescription { + case favedStickers(hash: Int64, packs: [Api.StickerPack], stickers: [Api.Document]) + case favedStickersNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .favedStickers(let hash, let packs, let stickers): + if boxed { + buffer.appendInt32(750063767) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(packs.count)) + for item in packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stickers.count)) + for item in stickers { + item.serialize(buffer, true) + } + break + case .favedStickersNotModified: + if boxed { + buffer.appendInt32(-1634752813) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .favedStickers(let hash, let packs, let stickers): + return ("favedStickers", [("hash", hash as Any), ("packs", packs as Any), ("stickers", stickers as Any)]) + case .favedStickersNotModified: + return ("favedStickersNotModified", []) + } + } + + public static func parse_favedStickers(_ reader: BufferReader) -> FavedStickers? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.Document]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.FavedStickers.favedStickers(hash: _1!, packs: _2!, stickers: _3!) + } + else { + return nil + } + } + public static func parse_favedStickersNotModified(_ reader: BufferReader) -> FavedStickers? { + return Api.messages.FavedStickers.favedStickersNotModified + } + + } +} +public extension Api.messages { + enum FeaturedStickers: TypeConstructorDescription { + case featuredStickers(flags: Int32, hash: Int64, count: Int32, sets: [Api.StickerSetCovered], unread: [Int64]) + case featuredStickersNotModified(count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .featuredStickers(let flags, let hash, let count, let sets, let unread): + if boxed { + buffer.appendInt32(-1103615738) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(hash, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(unread.count)) + for item in unread { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .featuredStickersNotModified(let count): + if boxed { + buffer.appendInt32(-958657434) + } + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .featuredStickers(let flags, let hash, let count, let sets, let unread): + return ("featuredStickers", [("flags", flags as Any), ("hash", hash as Any), ("count", count as Any), ("sets", sets as Any), ("unread", unread as Any)]) + case .featuredStickersNotModified(let count): + return ("featuredStickersNotModified", [("count", count as Any)]) + } + } + + public static func parse_featuredStickers(_ reader: BufferReader) -> FeaturedStickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + var _5: [Int64]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.FeaturedStickers.featuredStickers(flags: _1!, hash: _2!, count: _3!, sets: _4!, unread: _5!) + } + else { + return nil + } + } + public static func parse_featuredStickersNotModified(_ reader: BufferReader) -> FeaturedStickers? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.FeaturedStickers.featuredStickersNotModified(count: _1!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum ForumTopics: TypeConstructorDescription { case forumTopics(flags: Int32, count: Int32, topics: [Api.ForumTopic], messages: [Api.Message], chats: [Api.Chat], users: [Api.User], pts: Int32) @@ -1490,61 +1642,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum SavedGifs: TypeConstructorDescription { - case savedGifs(hash: Int64, gifs: [Api.Document]) - case savedGifsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .savedGifs(let hash, let gifs): - if boxed { - buffer.appendInt32(-2069878259) - } - serializeInt64(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(gifs.count)) - for item in gifs { - item.serialize(buffer, true) - } - break - case .savedGifsNotModified: - if boxed { - buffer.appendInt32(-402498398) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .savedGifs(let hash, let gifs): - return ("savedGifs", [("hash", hash as Any), ("gifs", gifs as Any)]) - case .savedGifsNotModified: - return ("savedGifsNotModified", []) - } - } - - public static func parse_savedGifs(_ reader: BufferReader) -> SavedGifs? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.Document]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.SavedGifs.savedGifs(hash: _1!, gifs: _2!) - } - else { - return nil - } - } - public static func parse_savedGifsNotModified(_ reader: BufferReader) -> SavedGifs? { - return Api.messages.SavedGifs.savedGifsNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index 31b027aec5..bbd86ee717 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -1,3 +1,61 @@ +public extension Api.messages { + enum SavedGifs: TypeConstructorDescription { + case savedGifs(hash: Int64, gifs: [Api.Document]) + case savedGifsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedGifs(let hash, let gifs): + if boxed { + buffer.appendInt32(-2069878259) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(gifs.count)) + for item in gifs { + item.serialize(buffer, true) + } + break + case .savedGifsNotModified: + if boxed { + buffer.appendInt32(-402498398) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedGifs(let hash, let gifs): + return ("savedGifs", [("hash", hash as Any), ("gifs", gifs as Any)]) + case .savedGifsNotModified: + return ("savedGifsNotModified", []) + } + } + + public static func parse_savedGifs(_ reader: BufferReader) -> SavedGifs? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.Document]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.SavedGifs.savedGifs(hash: _1!, gifs: _2!) + } + else { + return nil + } + } + public static func parse_savedGifsNotModified(_ reader: BufferReader) -> SavedGifs? { + return Api.messages.SavedGifs.savedGifsNotModified + } + + } +} public extension Api.messages { enum SavedReactionTags: TypeConstructorDescription { case savedReactionTags(tags: [Api.SavedReactionTag], hash: Int64) @@ -1436,179 +1494,3 @@ public extension Api.payments { } } -public extension Api.payments { - enum PaymentReceipt: TypeConstructorDescription { - case paymentReceipt(flags: Int32, date: Int32, botId: Int64, providerId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, info: Api.PaymentRequestedInfo?, shipping: Api.ShippingOption?, tipAmount: Int64?, currency: String, totalAmount: Int64, credentialsTitle: String, users: [Api.User]) - case paymentReceiptStars(flags: Int32, date: Int32, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, currency: String, totalAmount: Int64, transactionId: String, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): - if boxed { - buffer.appendInt32(1891958275) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt64(botId, buffer: buffer, boxed: false) - serializeInt64(providerId, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {photo!.serialize(buffer, true)} - invoice.serialize(buffer, true) - if Int(flags) & Int(1 << 0) != 0 {info!.serialize(buffer, true)} - if Int(flags) & Int(1 << 1) != 0 {shipping!.serialize(buffer, true)} - if Int(flags) & Int(1 << 3) != 0 {serializeInt64(tipAmount!, buffer: buffer, boxed: false)} - serializeString(currency, buffer: buffer, boxed: false) - serializeInt64(totalAmount, buffer: buffer, boxed: false) - serializeString(credentialsTitle, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .paymentReceiptStars(let flags, let date, let botId, let title, let description, let photo, let invoice, let currency, let totalAmount, let transactionId, let users): - if boxed { - buffer.appendInt32(-625215430) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt64(botId, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {photo!.serialize(buffer, true)} - invoice.serialize(buffer, true) - serializeString(currency, buffer: buffer, boxed: false) - serializeInt64(totalAmount, buffer: buffer, boxed: false) - serializeString(transactionId, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): - return ("paymentReceipt", [("flags", flags as Any), ("date", date as Any), ("botId", botId as Any), ("providerId", providerId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("info", info as Any), ("shipping", shipping as Any), ("tipAmount", tipAmount as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("credentialsTitle", credentialsTitle as Any), ("users", users as Any)]) - case .paymentReceiptStars(let flags, let date, let botId, let title, let description, let photo, let invoice, let currency, let totalAmount, let transactionId, let users): - return ("paymentReceiptStars", [("flags", flags as Any), ("date", date as Any), ("botId", botId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("transactionId", transactionId as Any), ("users", users as Any)]) - } - } - - public static func parse_paymentReceipt(_ reader: BufferReader) -> PaymentReceipt? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - var _4: Int64? - _4 = reader.readInt64() - var _5: String? - _5 = parseString(reader) - var _6: String? - _6 = parseString(reader) - var _7: Api.WebDocument? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.WebDocument - } } - var _8: Api.Invoice? - if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.Invoice - } - var _9: Api.PaymentRequestedInfo? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo - } } - var _10: Api.ShippingOption? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _10 = Api.parse(reader, signature: signature) as? Api.ShippingOption - } } - var _11: Int64? - if Int(_1!) & Int(1 << 3) != 0 {_11 = reader.readInt64() } - var _12: String? - _12 = parseString(reader) - var _13: Int64? - _13 = reader.readInt64() - var _14: String? - _14 = parseString(reader) - var _15: [Api.User]? - if let _ = reader.readInt32() { - _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil - let _c12 = _12 != nil - let _c13 = _13 != nil - let _c14 = _14 != nil - let _c15 = _15 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { - return Api.payments.PaymentReceipt.paymentReceipt(flags: _1!, date: _2!, botId: _3!, providerId: _4!, title: _5!, description: _6!, photo: _7, invoice: _8!, info: _9, shipping: _10, tipAmount: _11, currency: _12!, totalAmount: _13!, credentialsTitle: _14!, users: _15!) - } - else { - return nil - } - } - public static func parse_paymentReceiptStars(_ reader: BufferReader) -> PaymentReceipt? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: Api.WebDocument? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.WebDocument - } } - var _7: Api.Invoice? - if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.Invoice - } - var _8: String? - _8 = parseString(reader) - var _9: Int64? - _9 = reader.readInt64() - var _10: String? - _10 = parseString(reader) - var _11: [Api.User]? - if let _ = reader.readInt32() { - _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - let _c10 = _10 != nil - let _c11 = _11 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { - return Api.payments.PaymentReceipt.paymentReceiptStars(flags: _1!, date: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, currency: _8!, totalAmount: _9!, transactionId: _10!, users: _11!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 22436f1ec2..b657ab7619 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -1,3 +1,179 @@ +public extension Api.payments { + enum PaymentReceipt: TypeConstructorDescription { + case paymentReceipt(flags: Int32, date: Int32, botId: Int64, providerId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, info: Api.PaymentRequestedInfo?, shipping: Api.ShippingOption?, tipAmount: Int64?, currency: String, totalAmount: Int64, credentialsTitle: String, users: [Api.User]) + case paymentReceiptStars(flags: Int32, date: Int32, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, currency: String, totalAmount: Int64, transactionId: String, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): + if boxed { + buffer.appendInt32(1891958275) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt64(botId, buffer: buffer, boxed: false) + serializeInt64(providerId, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {photo!.serialize(buffer, true)} + invoice.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {info!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {shipping!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt64(tipAmount!, buffer: buffer, boxed: false)} + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(totalAmount, buffer: buffer, boxed: false) + serializeString(credentialsTitle, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .paymentReceiptStars(let flags, let date, let botId, let title, let description, let photo, let invoice, let currency, let totalAmount, let transactionId, let users): + if boxed { + buffer.appendInt32(-625215430) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt64(botId, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {photo!.serialize(buffer, true)} + invoice.serialize(buffer, true) + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(totalAmount, buffer: buffer, boxed: false) + serializeString(transactionId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): + return ("paymentReceipt", [("flags", flags as Any), ("date", date as Any), ("botId", botId as Any), ("providerId", providerId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("info", info as Any), ("shipping", shipping as Any), ("tipAmount", tipAmount as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("credentialsTitle", credentialsTitle as Any), ("users", users as Any)]) + case .paymentReceiptStars(let flags, let date, let botId, let title, let description, let photo, let invoice, let currency, let totalAmount, let transactionId, let users): + return ("paymentReceiptStars", [("flags", flags as Any), ("date", date as Any), ("botId", botId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("transactionId", transactionId as Any), ("users", users as Any)]) + } + } + + public static func parse_paymentReceipt(_ reader: BufferReader) -> PaymentReceipt? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + var _4: Int64? + _4 = reader.readInt64() + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) + var _7: Api.WebDocument? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.WebDocument + } } + var _8: Api.Invoice? + if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.Invoice + } + var _9: Api.PaymentRequestedInfo? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo + } } + var _10: Api.ShippingOption? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _10 = Api.parse(reader, signature: signature) as? Api.ShippingOption + } } + var _11: Int64? + if Int(_1!) & Int(1 << 3) != 0 {_11 = reader.readInt64() } + var _12: String? + _12 = parseString(reader) + var _13: Int64? + _13 = reader.readInt64() + var _14: String? + _14 = parseString(reader) + var _15: [Api.User]? + if let _ = reader.readInt32() { + _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil + let _c12 = _12 != nil + let _c13 = _13 != nil + let _c14 = _14 != nil + let _c15 = _15 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { + return Api.payments.PaymentReceipt.paymentReceipt(flags: _1!, date: _2!, botId: _3!, providerId: _4!, title: _5!, description: _6!, photo: _7, invoice: _8!, info: _9, shipping: _10, tipAmount: _11, currency: _12!, totalAmount: _13!, credentialsTitle: _14!, users: _15!) + } + else { + return nil + } + } + public static func parse_paymentReceiptStars(_ reader: BufferReader) -> PaymentReceipt? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Api.WebDocument? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.WebDocument + } } + var _7: Api.Invoice? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.Invoice + } + var _8: String? + _8 = parseString(reader) + var _9: Int64? + _9 = reader.readInt64() + var _10: String? + _10 = parseString(reader) + var _11: [Api.User]? + if let _ = reader.readInt32() { + _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + let _c11 = _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return Api.payments.PaymentReceipt.paymentReceiptStars(flags: _1!, date: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, currency: _8!, totalAmount: _9!, transactionId: _10!, users: _11!) + } + else { + return nil + } + } + + } +} public extension Api.payments { indirect enum PaymentResult: TypeConstructorDescription { case paymentResult(updates: Api.Updates) @@ -1568,113 +1744,3 @@ public extension Api.phone { } } -public extension Api.phone { - enum JoinAsPeers: TypeConstructorDescription { - case joinAsPeers(peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .joinAsPeers(let peers, let chats, let users): - if boxed { - buffer.appendInt32(-1343921601) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(peers.count)) - for item in peers { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .joinAsPeers(let peers, let chats, let users): - return ("joinAsPeers", [("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_joinAsPeers(_ reader: BufferReader) -> JoinAsPeers? { - var _1: [Api.Peer]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.phone.JoinAsPeers.joinAsPeers(peers: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.phone { - enum PhoneCall: TypeConstructorDescription { - case phoneCall(phoneCall: Api.PhoneCall, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .phoneCall(let phoneCall, let users): - if boxed { - buffer.appendInt32(-326966976) - } - phoneCall.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .phoneCall(let phoneCall, let users): - return ("phoneCall", [("phoneCall", phoneCall as Any), ("users", users as Any)]) - } - } - - public static func parse_phoneCall(_ reader: BufferReader) -> PhoneCall? { - var _1: Api.PhoneCall? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.PhoneCall - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.phone.PhoneCall.phoneCall(phoneCall: _1!, users: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index 3b5d24155d..4d5a192fea 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -1,3 +1,113 @@ +public extension Api.phone { + enum JoinAsPeers: TypeConstructorDescription { + case joinAsPeers(peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .joinAsPeers(let peers, let chats, let users): + if boxed { + buffer.appendInt32(-1343921601) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .joinAsPeers(let peers, let chats, let users): + return ("joinAsPeers", [("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_joinAsPeers(_ reader: BufferReader) -> JoinAsPeers? { + var _1: [Api.Peer]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.phone.JoinAsPeers.joinAsPeers(peers: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.phone { + enum PhoneCall: TypeConstructorDescription { + case phoneCall(phoneCall: Api.PhoneCall, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .phoneCall(let phoneCall, let users): + if boxed { + buffer.appendInt32(-326966976) + } + phoneCall.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .phoneCall(let phoneCall, let users): + return ("phoneCall", [("phoneCall", phoneCall as Any), ("users", users as Any)]) + } + } + + public static func parse_phoneCall(_ reader: BufferReader) -> PhoneCall? { + var _1: Api.PhoneCall? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.PhoneCall + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.phone.PhoneCall.phoneCall(phoneCall: _1!, users: _2!) + } + else { + return nil + } + } + + } +} public extension Api.photos { enum Photo: TypeConstructorDescription { case photo(photo: Api.Photo, users: [Api.User]) @@ -1384,141 +1494,3 @@ public extension Api.stories { } } -public extension Api.stories { - enum PeerStories: TypeConstructorDescription { - case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .peerStories(let stories, let chats, let users): - if boxed { - buffer.appendInt32(-890861720) - } - stories.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .peerStories(let stories, let chats, let users): - return ("peerStories", [("stories", stories as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_peerStories(_ reader: BufferReader) -> PeerStories? { - var _1: Api.PeerStories? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.PeerStories - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.stories.PeerStories.peerStories(stories: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum Stories: TypeConstructorDescription { - case stories(flags: Int32, count: Int32, stories: [Api.StoryItem], pinnedToTop: [Int32]?, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): - if boxed { - buffer.appendInt32(1673780490) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stories.count)) - for item in stories { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(pinnedToTop!.count)) - for item in pinnedToTop! { - serializeInt32(item, buffer: buffer, boxed: false) - }} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): - return ("stories", [("flags", flags as Any), ("count", count as Any), ("stories", stories as Any), ("pinnedToTop", pinnedToTop as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_stories(_ reader: BufferReader) -> Stories? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.StoryItem]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryItem.self) - } - var _4: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.stories.Stories.stories(flags: _1!, count: _2!, stories: _3!, pinnedToTop: _4, chats: _5!, users: _6!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 77f635e8bb..b9811b62c3 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -1,3 +1,141 @@ +public extension Api.stories { + enum PeerStories: TypeConstructorDescription { + case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .peerStories(let stories, let chats, let users): + if boxed { + buffer.appendInt32(-890861720) + } + stories.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .peerStories(let stories, let chats, let users): + return ("peerStories", [("stories", stories as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_peerStories(_ reader: BufferReader) -> PeerStories? { + var _1: Api.PeerStories? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.PeerStories + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.stories.PeerStories.peerStories(stories: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.stories { + enum Stories: TypeConstructorDescription { + case stories(flags: Int32, count: Int32, stories: [Api.StoryItem], pinnedToTop: [Int32]?, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): + if boxed { + buffer.appendInt32(1673780490) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stories.count)) + for item in stories { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(pinnedToTop!.count)) + for item in pinnedToTop! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stories(let flags, let count, let stories, let pinnedToTop, let chats, let users): + return ("stories", [("flags", flags as Any), ("count", count as Any), ("stories", stories as Any), ("pinnedToTop", pinnedToTop as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_stories(_ reader: BufferReader) -> Stories? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.StoryItem]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryItem.self) + } + var _4: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stories.Stories.stories(flags: _1!, count: _2!, stories: _3!, pinnedToTop: _4, chats: _5!, users: _6!) + } + else { + return nil + } + } + + } +} public extension Api.stories { enum StoryReactionsList: TypeConstructorDescription { case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index d421b4d691..6b3f1c13ec 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -6245,6 +6245,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getEmojiGameInfo() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-75592537) + + return (FunctionDescription(name: "messages.getEmojiGameInfo", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.EmojiGameInfo? in + let reader = BufferReader(buffer) + var result: Api.messages.EmojiGameInfo? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.EmojiGameInfo + } + return result + }) + } +} public extension Api.functions.messages { static func getEmojiGroups(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 7c0a3db5b1..fbc85e9e51 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -139,6 +139,7 @@ enum AccountStateMutationOperation { case UpdateMonoForumNoPaidException(peerId: PeerId, threadId: Int64, isFree: Bool) case UpdateStarGiftAuctionState(giftId: Int64, state: GiftAuctionContext.State.AuctionState) case UpdateStarGiftAuctionMyState(giftId: Int64, state: GiftAuctionContext.State.MyState) + case UpdateEmojiGameInfo(info: EmojiGameInfo) } struct HoleFromPreviousState { @@ -737,9 +738,13 @@ struct AccountMutableState { self.addOperation(.UpdateStarGiftAuctionMyState(giftId: giftId, state: state)) } + mutating func updateEmojiGameInfo(info: EmojiGameInfo) { + self.addOperation(.UpdateEmojiGameInfo(info: info)) + } + mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState, .UpdateEmojiGameInfo: break case let .AddMessages(messages, location): for message in messages { @@ -892,6 +897,7 @@ struct AccountReplayedFinalState { let addedConferenceInvitationMessagesIds: [MessageId] let updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] let updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] + let updatedEmojiGameInfo: EmojiGameInfo? } struct AccountFinalStateEvents { @@ -927,12 +933,13 @@ struct AccountFinalStateEvents { let addedConferenceInvitationMessagesIds: [MessageId] let updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] let updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] + let updatedEmojiGameInfo: EmojiGameInfo? var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.groupCallMessageUpdates.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedStarsBalance.isEmpty && self.updatedTonBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty && self.reportMessageDelivery.isEmpty && self.addedConferenceInvitationMessagesIds.isEmpty && self.updatedStarGiftAuctionState.isEmpty && self.updatedStarGiftAuctionMyState.isEmpty + return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.groupCallMessageUpdates.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedStarsBalance.isEmpty && self.updatedTonBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty && self.reportMessageDelivery.isEmpty && self.addedConferenceInvitationMessagesIds.isEmpty && self.updatedStarGiftAuctionState.isEmpty && self.updatedStarGiftAuctionMyState.isEmpty && self.updatedEmojiGameInfo == nil } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], groupCallMessageUpdates: [GroupCallMessageUpdate] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [PeerAndBoundThreadId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [PeerAndBoundThreadId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedTonBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set(), reportMessageDelivery: Set = Set(), addedConferenceInvitationMessagesIds: [MessageId] = [], updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] = [:], updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:]) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], groupCallMessageUpdates: [GroupCallMessageUpdate] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [PeerAndBoundThreadId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [PeerAndBoundThreadId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedTonBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set(), reportMessageDelivery: Set = Set(), addedConferenceInvitationMessagesIds: [MessageId] = [], updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] = [:], updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:], updatedEmojiGameInfo: EmojiGameInfo? = nil) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds @@ -965,6 +972,7 @@ struct AccountFinalStateEvents { self.addedConferenceInvitationMessagesIds = addedConferenceInvitationMessagesIds self.updatedStarGiftAuctionState = updatedStarGiftAuctionState self.updatedStarGiftAuctionMyState = updatedStarGiftAuctionMyState + self.updatedEmojiGameInfo = updatedEmojiGameInfo } init(state: AccountReplayedFinalState) { @@ -1000,6 +1008,7 @@ struct AccountFinalStateEvents { self.addedConferenceInvitationMessagesIds = state.addedConferenceInvitationMessagesIds self.updatedStarGiftAuctionState = state.updatedStarGiftAuctionState self.updatedStarGiftAuctionMyState = state.updatedStarGiftAuctionMyState + self.updatedEmojiGameInfo = state.updatedEmojiGameInfo } func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents { @@ -1068,7 +1077,8 @@ struct AccountFinalStateEvents { reportMessageDelivery: reportMessageDelivery, addedConferenceInvitationMessagesIds: addedConferenceInvitationMessagesIds, updatedStarGiftAuctionState: self.updatedStarGiftAuctionState.merging(other.updatedStarGiftAuctionState, uniquingKeysWith: { lhs, _ in lhs }), - updatedStarGiftAuctionMyState: self.updatedStarGiftAuctionMyState.merging(other.updatedStarGiftAuctionMyState, uniquingKeysWith: { lhs, _ in lhs }) + updatedStarGiftAuctionMyState: self.updatedStarGiftAuctionMyState.merging(other.updatedStarGiftAuctionMyState, uniquingKeysWith: { lhs, _ in lhs }), + updatedEmojiGameInfo: self.updatedEmojiGameInfo ) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 0602a0fc39..bdf7ff56a4 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -457,8 +457,15 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } return (TelegramMediaTodo(flags: flags, text: todoText, textEntities: todoEntities, items: list.map(TelegramMediaTodo.Item.init(apiItem:)), completions: todoCompletions), nil, nil, nil, nil, nil) } - case let .messageMediaDice(value, emoticon): - return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil, nil) + case let .messageMediaDice(_, value, emoticon, apiGameOutcome): + var gameOutcome: TelegramMediaDice.GameOutcome? + switch apiGameOutcome { + case let .emojiGameOutcome(seed, tonAmount): + gameOutcome = TelegramMediaDice.GameOutcome(seed: seed.makeData(), tonAmount: tonAmount) + default: + break + } + return (TelegramMediaDice(emoji: emoticon, value: value, gameOutcome: gameOutcome), nil, nil, nil, nil, nil) case let .messageMediaStory(flags, peerId, id, _): let isMention = (flags & (1 << 1)) != 0 return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil, nil) diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 1940b8d05e..4f78188e8c 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -343,8 +343,26 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post let inputTodo = Api.InputMedia.inputMediaTodo(todo: .todoList(flags: flags, title: .textWithEntities(text: todo.text, entities: apiEntitiesFromMessageTextEntities(todo.textEntities, associatedPeers: SimpleDictionary())), list: todo.items.map { $0.apiItem })) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputTodo, text), reuploadInfo: nil, cacheReferenceKey: nil))) } else if let dice = media as? TelegramMediaDice { - let inputDice = Api.InputMedia.inputMediaDice(emoticon: dice.emoji) - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputDice, text), reuploadInfo: nil, cacheReferenceKey: nil))) + if let tonAmount = dice.tonAmount { + let seedBytes = malloc(32)! + let _ = SecRandomCopyBytes(nil, 32, seedBytes.assumingMemoryBound(to: UInt8.self)) + let clientSeed = MemoryBuffer(memory: seedBytes, capacity: 32, length: 32, freeWhenDone: true) + + return postbox.transaction { transaction -> Signal in + let gameInfo = currentEmojiGameInfo(transaction: transaction) + if case let .available(info) = gameInfo { + let inputStakeDice = Api.InputMedia.inputMediaStakeDice(gameHash: info.gameHash, tonAmount: tonAmount, clientSeed: Buffer(buffer: clientSeed)) + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputStakeDice, text), reuploadInfo: nil, cacheReferenceKey: nil))) + } else { + return .fail(.generic) + } + } + |> castError(PendingMessageUploadError.self) + |> switchToLatest + } else { + let inputDice = Api.InputMedia.inputMediaDice(emoticon: dice.emoji) + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputDice, text), reuploadInfo: nil, cacheReferenceKey: nil))) + } } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content { var flags: Int32 = 0 flags |= 1 << 2 diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 4c82390309..7caf086c88 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1906,6 +1906,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } case let .updateStarGiftAuctionUserState(giftId, userState): updatedState.updateStarGiftAuctionMyState(giftId: giftId, state: GiftAuctionContext.State.MyState(apiAuctionUserState: userState)) + case let .updateEmojiGameInfo(info): + updatedState.updateEmojiGameInfo(info: EmojiGameInfo(apiEmojiGameInfo: info)) default: break } @@ -3629,7 +3631,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddQuickReplyMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState, .UpdateEmojiGameInfo: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -3772,6 +3774,7 @@ func replayFinalState( var reportMessageDelivery = Set() var updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] = [:] var updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:] + var updatedEmojiGameInfo: EmojiGameInfo? var holesFromPreviousStateMessageIds: [MessageId] = [] var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] @@ -5339,6 +5342,8 @@ func replayFinalState( updatedStarGiftAuctionState[giftId] = state case let .UpdateStarGiftAuctionMyState(giftId, state): updatedStarGiftAuctionMyState[giftId] = state + case let .UpdateEmojiGameInfo(info): + updatedEmojiGameInfo = info } } @@ -5889,6 +5894,7 @@ func replayFinalState( reportMessageDelivery: reportMessageDelivery, addedConferenceInvitationMessagesIds: addedConferenceInvitationMessagesIds, updatedStarGiftAuctionState: updatedStarGiftAuctionState, - updatedStarGiftAuctionMyState: updatedStarGiftAuctionMyState + updatedStarGiftAuctionMyState: updatedStarGiftAuctionMyState, + updatedEmojiGameInfo: updatedEmojiGameInfo ) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index e9cce703da..75c8b0179c 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -374,6 +374,7 @@ public final class AccountStateManager { private let appliedQtsPromise = Promise(nil) private let appliedQtsDisposable = MetaDisposable() private let reportMessageDeliveryDisposable = DisposableSet() + private let updateEmojiGameInfoDisposable = MetaDisposable() let updateConfigRequested: (() -> Void)? let isPremiumUpdated: (() -> Void)? @@ -414,6 +415,7 @@ public final class AccountStateManager { self.appliedMaxMessageIdDisposable.dispose() self.appliedQtsDisposable.dispose() self.reportMessageDeliveryDisposable.dispose() + self.updateEmojiGameInfoDisposable.dispose() } public func reset() { @@ -1137,6 +1139,11 @@ public final class AccountStateManager { if !events.updatedStarGiftAuctionMyState.isEmpty { strongSelf.notifyUpdatedStarGiftAuctionMyState(events.updatedStarGiftAuctionMyState) } + if let updatedEmojiGameInfo = events.updatedEmojiGameInfo { + strongSelf.updateEmojiGameInfoDisposable.set(strongSelf.postbox.transaction({ transaction in + updateEmojiGameInfo(transaction: transaction, { _ in return updatedEmojiGameInfo }) + }).start()) + } if !events.updatedCalls.isEmpty { for call in events.updatedCalls { strongSelf.callSessionManager?.updateSession(call, completion: { _ in }) diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index d470168d7f..1b64408434 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -125,6 +125,7 @@ final class AccountTaskManager { tasks.add(managedPeerColorUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedStarGiftsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start()) tasks.add(managedSavedMusicIdsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start()) + tasks.add(managedEmojiGameUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) self.managedTopReactionsDisposable.set(managedTopReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) diff --git a/submodules/TelegramCore/Sources/State/ManagedEmojiGameUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedEmojiGameUpdates.swift new file mode 100644 index 0000000000..710f78904b --- /dev/null +++ b/submodules/TelegramCore/Sources/State/ManagedEmojiGameUpdates.swift @@ -0,0 +1,104 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +public enum EmojiGameInfo: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case type + case info + } + + public struct Info: Codable, Equatable { + public let gameHash: String + public let previousStake: Int64 + public let currentStreak: Int32 + public let parameters: [Int32] + public let playsLeft: Int32? + } + + case available(Info) + case unavailable + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + + let type = try container.decode(Int32.self, forKey: .type) + switch type { + case 1: + self = .available(try container.decode(Info.self, forKey: .info)) + default: + self = .unavailable + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case let .available(info): + try container.encode(Int32(1), forKey: .type) + try container.encode(info, forKey: .info) + case .unavailable: + try container.encode(Int32(0), forKey: .type) + } + } +} + +extension EmojiGameInfo { + init(apiEmojiGameInfo: Api.messages.EmojiGameInfo) { + switch apiEmojiGameInfo { + case let .emojiGameDiceInfo(_, gameHash, prevStake, currentStreak, params, playsLeft): + self = .available(Info(gameHash: gameHash, previousStake: prevStake, currentStreak: currentStreak, parameters: params, playsLeft: playsLeft)) + case .emojiGameUnavailable: + self = .unavailable + } + } +} + + +public func currentEmojiGameInfo(transaction: Transaction) -> EmojiGameInfo { + if let entry = transaction.getPreferencesEntry(key: PreferencesKeys.emojiGameInfo())?.get(EmojiGameInfo.self) { + return entry + } else { + return .unavailable + } +} + +func updateEmojiGameInfo(transaction: Transaction, _ f: (EmojiGameInfo) -> EmojiGameInfo) { + let current = currentEmojiGameInfo(transaction: transaction) + let updated = f(current) + if updated != current { + transaction.setPreferencesEntry(key: PreferencesKeys.emojiGameInfo(), value: PreferencesEntry(updated)) + } +} + +func updateEmojiGameInfoOnce(postbox: Postbox, network: Network) -> Signal { + return network.request(Api.functions.messages.getEmojiGameInfo()) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + return postbox.transaction { transaction -> Void in + let info = EmojiGameInfo(apiEmojiGameInfo: result) + updateEmojiGameInfo(transaction: transaction) { _ in + return info + } + } + } +} + +func managedEmojiGameUpdates(postbox: Postbox, network: Network) -> Signal { + let poll = Signal { subscriber in + return updateEmojiGameInfoOnce(postbox: postbox, network: network).start(completed: { + subscriber.putCompletion() + }) + } + return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 0a8680497f..72cea0ce48 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 220 + return 221 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index b6a82d2f10..a6898bfbac 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -323,6 +323,7 @@ private enum PreferencesKeyValues: Int32 { case persistentChatInterfaceData = 45 case globalPostSearchState = 46 case savedMusicIds = 47 + case emojiGameInfo = 48 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -591,6 +592,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.savedMusicIds.rawValue) return key } + + public static func emojiGameInfo() -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.emojiGameInfo.rawValue) + return key + } } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaDice.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaDice.swift index d142f4d89d..c6bdf9d464 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaDice.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaDice.swift @@ -1,29 +1,57 @@ +import Foundation import Postbox public final class TelegramMediaDice: Media, Equatable { + public struct GameOutcome: Equatable { + let seed: Data + public let tonAmount: Int64 + } + public let emoji: String + public let tonAmount: Int64? public let value: Int32? + public let gameOutcome: GameOutcome? public let id: MediaId? = nil public let peerIds: [PeerId] = [] - public init(emoji: String, value: Int32? = nil) { + public init(emoji: String, tonAmount: Int64? = nil, value: Int32? = nil, gameOutcome: GameOutcome? = nil) { self.emoji = emoji + self.tonAmount = tonAmount self.value = value + self.gameOutcome = gameOutcome } public init(decoder: PostboxDecoder) { self.emoji = decoder.decodeStringForKey("e", orElse: "🎲") + self.tonAmount = decoder.decodeOptionalInt64ForKey("ta") self.value = decoder.decodeOptionalInt32ForKey("v") + if let seed = decoder.decodeDataForKey("gos"), let tonAmount = decoder.decodeOptionalInt64ForKey("goa") { + self.gameOutcome = GameOutcome(seed: seed, tonAmount: tonAmount) + } else { + self.gameOutcome = nil + } } public func encode(_ encoder: PostboxEncoder) { encoder.encodeString(self.emoji, forKey: "e") + if let tonAmount = self.tonAmount { + encoder.encodeInt64(tonAmount, forKey: "ta") + } else { + encoder.encodeNil(forKey: "ta") + } if let value = self.value { encoder.encodeInt32(value, forKey: "v") } else { encoder.encodeNil(forKey: "v") } + if let gameOutcome = self.gameOutcome { + encoder.encodeData(gameOutcome.seed, forKey: "gos") + encoder.encodeInt64(gameOutcome.tonAmount, forKey: "goa") + } else { + encoder.encodeNil(forKey: "gos") + encoder.encodeNil(forKey: "goa") + } } public static func ==(lhs: TelegramMediaDice, rhs: TelegramMediaDice) -> Bool { @@ -35,9 +63,15 @@ public final class TelegramMediaDice: Media, Equatable { if self.emoji != other.emoji { return false } + if self.tonAmount != other.tonAmount { + return false + } if self.value != other.value { return false } + if self.gameOutcome != other.gameOutcome { + return false + } return true } return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index e9d24a6cef..3724ff8b7a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -571,5 +571,26 @@ public extension TelegramEngine.EngineData.Item { return value } } + + public struct EmojiGame: TelegramEngineDataItem, PostboxViewDataItem { + public typealias Result = EmojiGameInfo + + public init() { + } + + var key: PostboxViewKey { + return .preferences(keys: Set([PreferencesKeys.emojiGameInfo()])) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? PreferencesView else { + preconditionFailure() + } + guard let emojiGameInfo = view.values[PreferencesKeys.emojiGameInfo()]?.get(EmojiGameInfo.self) else { + return .unavailable + } + return emojiGameInfo + } + } } } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index b1e4e9ace3..0734033617 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -193,12 +193,8 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case incomingVideoMessagePlayOnceTip = 62 case outgoingVideoMessagePlayOnceTip = 63 case savedMessageTagLabelSuggestion = 65 - case dismissedBusinessBadge = 68 case monetizationIntroDismissed = 70 case businessBotMessageTooltip = 71 - case dismissedBusinessIntroBadge = 72 - case dismissedBusinessLinksBadge = 73 - case dismissedBusinessChatbotsBadge = 74 case captionAboveMediaTooltip = 75 case channelSendGiftTooltip = 76 case starGiftWearTips = 77 @@ -519,10 +515,6 @@ private struct ApplicationSpecificNoticeKeys { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.savedMessageTagLabelSuggestion.key) } - static func dismissedBusinessBadge() -> NoticeEntryKey { - return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessBadge.key) - } - static func dismissedBirthdayPremiumGiftTip(peerId: PeerId) -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: dismissedBirthdayPremiumGiftTipNamespace), key: noticeKey(peerId: peerId, key: 0)) } @@ -543,18 +535,6 @@ private struct ApplicationSpecificNoticeKeys { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.businessBotMessageTooltip.key) } - static func dismissedBusinessIntroBadge() -> NoticeEntryKey { - return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessIntroBadge.key) - } - - static func dismissedBusinessLinksBadge() -> NoticeEntryKey { - return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessLinksBadge.key) - } - - static func dismissedBusinessChatbotsBadge() -> NoticeEntryKey { - return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessChatbotsBadge.key) - } - static func captionAboveMediaTooltip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.captionAboveMediaTooltip.key) } @@ -2154,27 +2134,6 @@ public struct ApplicationSpecificNotice { return Int(previousValue) } } - - public static func setDismissedBusinessBadge(accountManager: AccountManager) -> Signal { - return accountManager.transaction { transaction -> Void in - if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { - transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessBadge(), entry) - } - } - |> ignoreValues - } - - public static func dismissedBusinessBadge(accountManager: AccountManager) -> Signal { - return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessBadge()) - |> map { view -> Bool in - if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { - return true - } else { - return false - } - } - |> take(1) - } public static func dismissedBirthdayPremiumGiftTip(accountManager: AccountManager, peerId: PeerId) -> Signal { return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBirthdayPremiumGiftTip(peerId: peerId)) @@ -2286,69 +2245,6 @@ public struct ApplicationSpecificNotice { } } - public static func setDismissedBusinessLinksBadge(accountManager: AccountManager) -> Signal { - return accountManager.transaction { transaction -> Void in - if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { - transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessLinksBadge(), entry) - } - } - |> ignoreValues - } - - public static func dismissedBusinessLinksBadge(accountManager: AccountManager) -> Signal { - return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessLinksBadge()) - |> map { view -> Bool in - if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { - return true - } else { - return false - } - } - |> take(1) - } - - public static func setDismissedBusinessIntroBadge(accountManager: AccountManager) -> Signal { - return accountManager.transaction { transaction -> Void in - if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { - transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessIntroBadge(), entry) - } - } - |> ignoreValues - } - - public static func dismissedBusinessIntroBadge(accountManager: AccountManager) -> Signal { - return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessIntroBadge()) - |> map { view -> Bool in - if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { - return true - } else { - return false - } - } - |> take(1) - } - - public static func setDismissedBusinessChatbotsBadge(accountManager: AccountManager) -> Signal { - return accountManager.transaction { transaction -> Void in - if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { - transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessChatbotsBadge(), entry) - } - } - |> ignoreValues - } - - public static func dismissedBusinessChatbotsBadge(accountManager: AccountManager) -> Signal { - return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessChatbotsBadge()) - |> map { view -> Bool in - if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { - return true - } else { - return false - } - } - |> take(1) - } - public static func getCaptionAboveMediaTooltip(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Int32 in if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.captionAboveMediaTooltip())?.get(ApplicationSpecificCounterNotice.self) { diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift index 04a7a16029..9843334806 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift @@ -107,7 +107,7 @@ public final class PermissionContentNode: ASDisplayNode { self.textNode.displaysAsynchronously = false self.textNode.isAccessibilityElement = true - self.actionButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 9.0, isShimmering: true) + self.actionButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), glass: true, height: 52.0, cornerRadius: 26.0, isShimmering: true) self.footerNode = ImmediateTextNode() self.footerNode.textAlignment = .center diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift index 5d9e099c44..d0c2f05323 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift @@ -36,15 +36,17 @@ public final class PermissionController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.splashScreen = splashScreen - let navigationBarPresentationData: NavigationBarPresentationData - if splashScreen { - navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) - } else { - navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) - } +// let navigationBarPresentationData: NavigationBarPresentationData +// if splashScreen { +// navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, style: .glass), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) +// } else { + let navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, style: .glass) +// } super.init(navigationBarPresentationData: navigationBarPresentationData) + self._hasGlassStyle = true + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.updateThemeAndStrings() @@ -86,15 +88,15 @@ public final class PermissionController: ViewController { private func updateThemeAndStrings() { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - let navigationBarPresentationData: NavigationBarPresentationData - if self.splashScreen { - navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) - } else { - navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) - } +// let navigationBarPresentationData: NavigationBarPresentationData +// if self.splashScreen { +// navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) +// } else { + let navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, style: .glass) +// } self.navigationBar?.updatePresentationData(navigationBarPresentationData, transition: .immediate) - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil) + //self.navigationItem.backBarButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil) if self.navigationItem.rightBarButtonItem != nil { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Permissions_Skip, style: .plain, target: self, action: #selector(PermissionController.nextPressed)) } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index c139bcc6ad..5a0c2ea7fb 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1764,6 +1764,17 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, resultTitleString = strings.Conversation_StoryExpiredMentionTextOutgoing(compactPeerName) } attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else if let dice = media as? TelegramMediaDice, let gameOutcome = dice.gameOutcome { + if let value = dice.value, value > 1 { + //TODO:localize + let value = formatTonAmountText(gameOutcome.tonAmount, dateTimeFormat: dateTimeFormat) + let attributedText = NSMutableAttributedString(string: "You won $\(value)", font: titleFont, textColor: primaryTextColor) + if let range = attributedText.string.range(of: "$") { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: true)), range: NSRange(range, in: attributedText.string)) + attributedText.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedText.string)) + } + attributedString = attributedText + } } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 0b4b6a185b..3ea6a6b64d 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -508,6 +508,8 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode", "//submodules/TelegramUI/Components/Settings/PasskeysScreen", "//submodules/TelegramUI/Components/Gifts/GiftDemoScreen", + "//submodules/TelegramUI/Components/EmojiGameStakeScreen", + "//submodules/TelegramUI/Components/AlertComponent", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AlertComponent/BUILD b/submodules/TelegramUI/Components/AlertComponent/BUILD index 076bdbcc53..0fc1a866f5 100644 --- a/submodules/TelegramUI/Components/AlertComponent/BUILD +++ b/submodules/TelegramUI/Components/AlertComponent/BUILD @@ -12,9 +12,17 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/TelegramPresentationData", "//submodules/ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/Markdown", + "//submodules/TextFormat", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/ActivityIndicatorComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift new file mode 100644 index 0000000000..dd32935321 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift @@ -0,0 +1,195 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import MultilineTextComponent +import GlassBackgroundComponent +import ActivityIndicatorComponent + +private let titleFont = Font.medium(17.0) +private let boldTitleFont = Font.semibold(17.0) + +final class AlertActionComponent: Component { + typealias EnvironmentType = AlertComponentEnvironment + + static let actionHeight: CGFloat = 48.0 + + struct Theme: Equatable { + enum Font { + case regular + case bold + } + + let background: UIColor + let foreground: UIColor + let secondary: UIColor + let font: Font + } + + let theme: Theme + let title: String + let isHighlighted: Bool + let progress: Signal + + init( + theme: Theme, + title: String, + isHighlighted: Bool, + progress: Signal + ) { + self.theme = theme + self.title = title + self.isHighlighted = isHighlighted + self.progress = progress + } + + static func ==(lhs: AlertActionComponent, rhs: AlertActionComponent) -> Bool { + if lhs.theme != rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isHighlighted != rhs.isHighlighted { + return false + } + return true + } + + final class View: UIView { + private let backgroundView = UIView() + private let title = ComponentView() + private var activity: ComponentView? + + private var component: AlertActionComponent? + private weak var state: EmptyComponentState? + + private var progressDisposable: Disposable? + private var hasProgress = false + + private var isUpdating = false + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundView.clipsToBounds = true + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func update(component: AlertActionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + if self.component == nil { + self.progressDisposable = (component.progress + |> deliverOnMainQueue).start(next: { [weak self] hasProgress in + guard let self else { + return + } + self.hasProgress = hasProgress + if !self.isUpdating { + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + }) + } + self.component = component + self.state = state + + let attributedString = NSMutableAttributedString(string: component.title, font: component.theme.font == .bold ? boldTitleFont : titleFont, textColor: .white, paragraphAlignment: .center) + if let range = attributedString.string.range(of: "$") { + attributedString.addAttribute(.attachment, value: UIImage(bundleImageName: "Item List/PremiumIcon")!, range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedString.string)) + } + + let titlePadding: CGFloat = 16.0 + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 1, + tintColor: component.theme.foreground + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - titlePadding * 2.0, height: availableSize.height) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.bounds = CGRect(origin: .zero, size: titleSize) + transition.setAlpha(view: titleView, alpha: self.hasProgress ? 0.0 : 1.0) + } + + if self.hasProgress { + let activity: ComponentView + if let current = self.activity { + activity = current + } else { + activity = ComponentView() + self.activity = activity + } + let activitySize = activity.update( + transition: transition, + component: AnyComponent(ActivityIndicatorComponent(color: component.theme.secondary)), + environment: {}, + containerSize: availableSize + ) + if let activityView = activity.view { + if activityView.superview == nil { + self.addSubview(activityView) + transition.animateAlpha(view: activityView, from: 0.0, to: 1.0) + } + activityView.bounds = CGRect(origin: .zero, size: activitySize) + } + } else if let activity = self.activity { + self.activity = nil + if let activityView = activity.view { + transition.setAlpha(view: activityView, alpha: 0.0, completion: { _ in + activityView.removeFromSuperview() + }) + } + } + + transition.setBackgroundColor(view: self.backgroundView, color: component.theme.background) + transition.setAlpha(view: self.backgroundView, alpha: component.isHighlighted ? 0.35 : 1.0) + self.backgroundView.layer.cornerRadius = availableSize.height * 0.5 + + return CGSize(width: titleSize.width + titlePadding * 2.0, height: availableSize.height) + } + + func applySize(size: CGSize, transition: ComponentTransition) { + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size)) + + if let titleView = self.title.view { + let titleSize = titleView.bounds.size + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + transition.setFrame(view: titleView, frame: titleFrame) + } + + if let activityView = self.activity?.view { + let activitySize = activityView.bounds.size + let activityFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - activitySize.width) / 2.0), y: floorToScreenPixels((size.height - activitySize.height) / 2.0)), size: activitySize) + transition.setFrame(view: activityView, frame: activityFrame) + } + } + } + + 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/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift index d7b9b0668f..cc21864908 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -1,395 +1,864 @@ import Foundation import UIKit -import Display -import TelegramPresentationData -import ComponentFlow -import ComponentDisplayAdapters import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import MultilineTextComponent +import ViewControllerComponent +import ComponentDisplayAdapters +import GlassBackgroundComponent -private let alertWidth: CGFloat = 270.0 - -public enum ComponentAlertActionType { - case genericAction - case defaultAction - case destructiveAction - case defaultDestructiveAction -} - -public struct ComponentAlertAction { - public let type: ComponentAlertActionType - public let title: String - public let action: () -> Void +public final class AlertComponentEnvironment: Equatable { + public let theme: PresentationTheme - public init(type: ComponentAlertActionType, title: String, action: @escaping () -> Void) { - self.type = type - self.title = title - self.action = action - } -} - -public final class ComponentAlertContentActionNode: HighlightableButtonNode { - private var theme: AlertControllerTheme - public var action: ComponentAlertAction { - didSet { - self.updateTitle() - } - } - - private let backgroundNode: ASDisplayNode - - public var highlightedUpdated: (Bool) -> Void = { _ in } - - public init(theme: AlertControllerTheme, action: ComponentAlertAction) { + public init(theme: PresentationTheme) { self.theme = theme - self.action = action - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.alpha = 0.0 - - super.init() - - self.titleNode.maximumNumberOfLines = 2 - - self.highligthedChanged = { [weak self] value in - if let strongSelf = self { - strongSelf.setHighlighted(value, animated: true) - } + } + + public static func ==(lhs: AlertComponentEnvironment, rhs: AlertComponentEnvironment) -> Bool { + if lhs.theme !== rhs.theme { + return false } - - self.updateTheme(theme) - } - - public override func didLoad() { - super.didLoad() - - self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) - - self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in - if let strongSelf = self { - strongSelf.setHighlighted(true, animated: false) - } - }, willExit: { [weak self] in - if let strongSelf = self { - strongSelf.setHighlighted(false, animated: false) - } - }) - } - - public func performAction() { - if self.actionEnabled { - self.action.action() - } - } - - public func setHighlighted(_ highlighted: Bool, animated: Bool) { - self.highlightedUpdated(highlighted) - if highlighted { - if self.backgroundNode.supernode == nil { - self.insertSubnode(self.backgroundNode, at: 0) - } - self.backgroundNode.alpha = 1.0 - } else { - if animated { - UIView.animate(withDuration: 0.3, animations: { - self.backgroundNode.alpha = 0.0 - }) - } else { - self.backgroundNode.alpha = 0.0 - } - } - } - public var actionEnabled: Bool = true { - didSet { - self.isUserInteractionEnabled = self.actionEnabled - self.updateTitle() - } - } - - public func updateTheme(_ theme: AlertControllerTheme) { - self.theme = theme - self.backgroundNode.backgroundColor = theme.highlightedItemColor - self.updateTitle() - } - - private func updateTitle() { - var font = Font.regular(theme.baseFontSize) - var color: UIColor - switch self.action.type { - case .defaultAction, .genericAction: - color = self.actionEnabled ? self.theme.accentColor : self.theme.disabledColor - case .destructiveAction, .defaultDestructiveAction: - color = self.actionEnabled ? self.theme.destructiveColor : self.theme.disabledColor - } - switch self.action.type { - case .defaultAction, .defaultDestructiveAction: - font = Font.semibold(theme.baseFontSize) - case .destructiveAction, .genericAction: - break - } - self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: []) - self.accessibilityLabel = self.action.title - self.accessibilityTraits = [.button] - } - - @objc func pressed() { - self.action.action() - } - - override public func layout() { - super.layout() - - self.backgroundNode.frame = self.bounds + return true } } -public enum ComponentAlertContentActionLayout { - case horizontal - case vertical -} - -public final class ComponentAlertContentNode: AlertContentNode { - private var theme: AlertControllerTheme - private let actionLayout: ComponentAlertContentActionLayout +private final class AlertScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment - private let content: AnyComponent - private let contentView = ComponentView() + let configuration: AlertScreen.Configuration + let content: [AnyComponentWithIdentity] + let actions: [AlertScreen.Action] + let ready: Promise - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [ComponentAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - private let _dismissOnOutsideTap: Bool - override public var dismissOnOutsideTap: Bool { - return self._dismissOnOutsideTap - } - - private var highlightedItemIndex: Int? = nil - - public init(theme: AlertControllerTheme, content: AnyComponent, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout, dismissOnOutsideTap: Bool) { - self.theme = theme - self.actionLayout = actionLayout - self._dismissOnOutsideTap = dismissOnOutsideTap + init( + configuration: AlertScreen.Configuration, + content: [AnyComponentWithIdentity], + actions: [AlertScreen.Action], + ready: Promise + ) { + self.configuration = configuration self.content = content + self.actions = actions + self.ready = ready + } + + static func ==(lhs: AlertScreenComponent, rhs: AlertScreenComponent) -> Bool { + return true + } + + enum KeyCommand { + case up + case down + case left + case right + case escape + case enter + } + + final class View: UIView, UIGestureRecognizerDelegate { + private let dimView = UIView() + private let containerView = GlassBackgroundContainerView() + private let backgroundView = GlassBackgroundView() - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isUserInteractionEnabled = false - self.actionNodesSeparator.backgroundColor = theme.separatorColor + private var content: [AnyHashable: ComponentView] = [:] + private var actions: [AnyHashable: ComponentView] = [:] - self.actionNodes = actions.map { action -> ComponentAlertContentActionNode in - return ComponentAlertContentActionNode(theme: theme, action: action) - } - - var actionVerticalSeparators: [ASDisplayNode] = [] - if actions.count > 1 { - for _ in 0 ..< actions.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - separatorNode.backgroundColor = theme.separatorColor - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.actionNodesSeparator) - - var i = 0 - for actionNode in self.actionNodes { - self.addSubnode(actionNode) + private var highlightedAction: AnyHashable? + private let hapticFeedback = HapticFeedback() + + private enum ActionLayout { + case horizontal + case vertical + case verticalReversed - let index = i - actionNode.highlightedUpdated = { [weak self] highlighted in - if highlighted { - self?.highlightedItemIndex = index + var isVertical: Bool { + switch self { + case .vertical, .verticalReversed: + return true + default: + return false } } - i += 1 + } + private var effectiveActionLayout: ActionLayout = .horizontal + + fileprivate var dismissedByTapOutside = false + + private var isUpdating: Bool = false + private var component: AlertScreenComponent? + private var environment: EnvironmentType? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.dimView.alpha = 0.0 + self.dimView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2) + + self.addSubview(self.dimView) + self.addSubview(self.containerView) + self.containerView.contentView.addSubview(self.backgroundView) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapped))) + + let tapRecognizer = ActionSelectionGestureRecognizer(target: self, action: #selector(self.actionTapped(_:))) + tapRecognizer.delegate = self + self.backgroundView.addGestureRecognizer(tapRecognizer) } - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) + required init?(coder: NSCoder) { + preconditionFailure() } - } - - func setHighlightedItemIndex(_ index: Int?, update: Bool = false) { - self.highlightedItemIndex = index - if update { - var i = 0 - for actionNode in self.actionNodes { - if i == index { - actionNode.setHighlighted(true, animated: false) - } else { - actionNode.setHighlighted(false, animated: false) + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is ActionSelectionGestureRecognizer { + let location = gestureRecognizer.location(in: self.backgroundView) + for (_, action) in self.actions { + if let actionView = action.view, actionView.frame.contains(location) { + return true + } } - i += 1 + return false + } else { + return super.gestureRecognizerShouldBegin(gestureRecognizer) } } - } - - override public func decreaseHighlightedIndex() { - let currentHighlightedIndex = self.highlightedItemIndex ?? 0 - self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true) - } - - override public func increaseHighlightedIndex() { - let currentHighlightedIndex = self.highlightedItemIndex ?? -1 - - self.setHighlightedItemIndex(min(self.actionNodes.count - 1, currentHighlightedIndex + 1), update: true) - } - - override public func performHighlightedAction() { - guard let highlightedItemIndex = self.highlightedItemIndex else { - return + @objc private func actionTapped(_ gestureRecognizer: ActionSelectionGestureRecognizer) { + let location = gestureRecognizer.location(in: self.backgroundView) + switch gestureRecognizer.state { + case .began, .changed: + var highlightedActionId: AnyHashable? + for (actionId, action) in self.actions { + if let actionView = action.view, actionView.frame.contains(location) { + highlightedActionId = actionId + break + } + } + if self.highlightedAction != highlightedActionId { + self.highlightedAction = highlightedActionId + self.state?.updated(transition: .easeInOut(duration: 0.2)) + + if case .changed = gestureRecognizer.state, highlightedActionId != nil { + self.hapticFeedback.tap() + } + } + case .ended: + if let _ = self.highlightedAction { + self.performHighlightedAction() + self.highlightedAction = nil + self.state?.updated(transition: .easeInOut(duration: 0.2)) + } + case .cancelled: + self.highlightedAction = nil + self.state?.updated(transition: .easeInOut(duration: 0.2)) + default: + break + } } - var i = 0 - for itemNode in self.actionNodes { - if i == highlightedItemIndex { - itemNode.performAction() + @objc private func dimTapped() { + guard let component = self.component, component.configuration.dismissOnOutsideTap else { return } - i += 1 - } - } - - override public func updateTheme(_ theme: AlertControllerTheme) { - self.theme = theme - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor + self.dismissedByTapOutside = true + self.requestDismiss() } - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) + func animateIn() { + let alphaTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .linear)) + let scaleTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) + alphaTransition.setAlpha(view: self.dimView, alpha: 1.0) + + scaleTransition.animateScale(view: self.backgroundView, from: 1.15, to: 1.0) + alphaTransition.animateAlpha(view: self.containerView, from: 0.0, to: 1.0) } - } - - override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - self.validLayout = size - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) - - var size = size - size.width = min(size.width, alertWidth) - - let contentSize = self.contentView.update( - transition: ComponentTransition(transition), - component: self.content, - environment: {}, - containerSize: CGSize(width: size.width - insets.left - insets.right, height: 10000.0) - ) - - let actionButtonHeight: CGFloat = 44.0 - - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = self.actionLayout - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { - effectiveActionLayout = .vertical + func animateOut(completion: @escaping () -> Void) { + let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .linear)) + transition.setAlpha(view: self.dimView, alpha: 0.0, completion: { _ in + completion() + }) + var initialAlpha: CGFloat = 1.0 + if let presentationLayer = self.containerView.layer.presentation() { + initialAlpha = CGFloat(presentationLayer.opacity) } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + self.containerView.layer.animateAlpha(from: initialAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self.containerView.contentView { + return self.dimView + } + return result + } + + func requestDismiss() { + guard let controller = self.environment?.controller() as? AlertScreen else { + return + } + controller.dismiss(completion: nil) + } + + func handleKeyCommand(_ command: KeyCommand) { + switch command { + case .up: + guard self.effectiveActionLayout.isVertical else { + return + } + self.updateActionHighlight(previous: false) + case .down: + guard self.effectiveActionLayout.isVertical else { + return + } + self.updateActionHighlight(previous: true) + case .left: + guard !self.effectiveActionLayout.isVertical else { + return + } + self.updateActionHighlight(previous: true) + case .right: + guard !self.effectiveActionLayout.isVertical else { + return + } + self.updateActionHighlight(previous: false) + case .escape: + self.requestDismiss() + case .enter: + self.performHighlightedAction() } } - let resultSize: CGSize - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let contentWidth = alertWidth - insets.left - insets.right - - let contentFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - contentSize.width) / 2.0), y: insets.top), size: contentSize) - if let contentComponentView = self.contentView.view { - if contentComponentView.superview == nil { - self.view.insertSubview(contentComponentView, belowSubview: self.actionNodesSeparator.view) - transition.updateFrame(view: contentComponentView, frame: contentFrame) + func updateActionHighlight(previous: Bool) { + guard let component = self.component else { + return } - } - - resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: contentSize.height + actionsHeight + insets.top + insets.bottom) - - self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - switch effectiveActionLayout { - case .horizontal: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - case .vertical: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + guard let highlightedAction = self.highlightedAction else { + if let action = component.actions.first(where: { $0.type == .default }) { + self.highlightedAction = action.id + } else if let action = component.actions.first(where: { $0.type == .defaultDestructive }) { + self.highlightedAction = action.id + } else if case .verticalReversed = self.effectiveActionLayout, let action = component.actions.last { + self.highlightedAction = action.id + } else if let action = component.actions.first { + self.highlightedAction = action.id + } + self.state?.updated(transition: .easeInOut(duration: 0.2)) + return + } + + let sequence = previous ? component.actions.reversed() : component.actions + var selectNext = false + var newHighlightedAction: AnyHashable? + + for action in sequence { + let id = AnyHashable(action.id) + if selectNext { + newHighlightedAction = id + break + } else if id == highlightedAction { + selectNext = true } } - separatorIndex += 1 - - let currentActionWidth: CGFloat - switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical: - currentActionWidth = resultSize.width + guard let newHighlightedAction else { + return } - - let actionNodeFrame: CGRect - switch effectiveActionLayout { - case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } - - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 + self.highlightedAction = newHighlightedAction + self.state?.updated(transition: .easeInOut(duration: 0.2)) } - return resultSize + func performHighlightedAction() { + guard let component = self.component else { + return + } + guard let highlightedAction = self.highlightedAction else { + return + } + guard let action = component.actions.first(where: { AnyHashable($0.id) == highlightedAction }) else { + return + } + action.action() + if action.autoDismiss { + self.requestDismiss() + } + } + + func update(component: AlertScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + self.state = state + + if self.component == nil { + + } + self.component = component + + var alertHeight: CGFloat = 0.0 + let alertWidth: CGFloat = 300.0 + let contentTopInset: CGFloat = 22.0 + let contentBottomInset: CGFloat = 21.0 + let contentSideInset: CGFloat = 30.0 + let contentSpacing: CGFloat = 8.0 + let actionSideInset: CGFloat = 16.0 + let actionSpacing: CGFloat = 8.0 + let fullWidthActionSize = CGSize(width: alertWidth - actionSideInset * 2.0, height: AlertActionComponent.actionHeight) + let halfWidthActionSize = CGSize(width: (alertWidth - actionSideInset * 2.0 - actionSpacing) / 2.0, height: AlertActionComponent.actionHeight) + + let alertEnvironment = AlertComponentEnvironment(theme: environment.theme) + + var contentOriginY: CGFloat = 0.0 + var validContentIds: Set = Set() + for content in component.content { + if contentOriginY.isZero { + contentOriginY += contentTopInset + } else { + contentOriginY += contentSpacing + } + validContentIds.insert(content.id) + + let item: ComponentView + var itemTransition = transition + if let current = self.content[content.id] { + item = current + } else { + item = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.content[content.id] = item + } + + let itemSize = item.update( + transition: itemTransition, + component: content.component, + environment: { alertEnvironment }, + containerSize: CGSize(width: alertWidth - contentSideInset * 2.0, height: availableSize.height) + ) + let itemFrame = CGRect(origin: CGPoint(x: contentSideInset, y: contentOriginY), size: itemSize) + if let itemView = item.view { + if itemView.superview == nil { + self.backgroundView.contentView.addSubview(itemView) + } + transition.setFrame(view: itemView, frame: itemFrame) + } + contentOriginY += itemSize.height + } + + if !contentOriginY.isZero { + alertHeight += contentOriginY + alertHeight += contentBottomInset + } + + let genericActionTheme = AlertActionComponent.Theme( + background: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), + foreground: environment.theme.actionSheet.primaryTextColor, + secondary: environment.theme.actionSheet.secondaryTextColor, + font: .regular + ) + let defaultActionTheme = AlertActionComponent.Theme( + background: environment.theme.actionSheet.controlAccentColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + secondary: environment.theme.actionSheet.secondaryTextColor, + font: .bold + ) + let destructiveActionTheme = AlertActionComponent.Theme( + background: environment.theme.list.itemDestructiveColor, + foreground: .white, + secondary: .white.withMultipliedAlpha(0.6), + font: .regular + ) + let defaultDestructiveActionTheme = AlertActionComponent.Theme( + background: environment.theme.list.itemDestructiveColor, + foreground: .white, + secondary: .white.withMultipliedAlpha(0.6), + font: .bold + ) + + var effectiveActionLayout: ActionLayout = .horizontal + if case .vertical = component.configuration.actionAlignment { + effectiveActionLayout = .vertical + } + var validActionIds: Set = Set() + for action in component.actions { + validActionIds.insert(action.id) + + let item: ComponentView + var itemTransition = transition + if let current = self.actions[action.id] { + item = current + } else { + item = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.actions[action.id] = item + } + + let actionTheme: AlertActionComponent.Theme + switch action.type { + case .generic: + actionTheme = genericActionTheme + case .default: + actionTheme = defaultActionTheme + case .destructive: + actionTheme = destructiveActionTheme + case .defaultDestructive: + actionTheme = defaultDestructiveActionTheme + } + let itemSize = item.update( + transition: itemTransition, + component: AnyComponent(AlertActionComponent( + theme: actionTheme, + title: action.title, + isHighlighted: AnyHashable(action.id) == self.highlightedAction, + progress: action.progressPromise.get() + )), + environment: { alertEnvironment }, + containerSize: fullWidthActionSize + ) + if let itemView = item.view { + if itemView.superview == nil { + self.backgroundView.contentView.addSubview(itemView) + } + } + + if case .horizontal = effectiveActionLayout, itemSize.width > halfWidthActionSize.width { + effectiveActionLayout = .verticalReversed + } + } + self.effectiveActionLayout = effectiveActionLayout + + if !component.actions.isEmpty { + let actionsHeight: CGFloat + if self.effectiveActionLayout.isVertical { + actionsHeight = fullWidthActionSize.height * CGFloat(component.actions.count) + actionSpacing * CGFloat(component.actions.count - 1) + } else { + actionsHeight = fullWidthActionSize.height + } + alertHeight += actionsHeight + alertHeight += actionSideInset + } + + var actionOriginX: CGFloat = actionSideInset + var actionOriginY: CGFloat + switch self.effectiveActionLayout { + case .horizontal, .verticalReversed: + actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height + case .vertical: + actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height * CGFloat(component.actions.count) - actionSpacing * CGFloat(component.actions.count - 1) + } + for action in component.actions { + guard let item = self.actions[action.id], let itemView = item.view as? AlertActionComponent.View else { + continue + } + let itemFrame: CGRect + switch self.effectiveActionLayout { + case .horizontal: + itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: halfWidthActionSize) + actionOriginX += halfWidthActionSize.width + actionSpacing + case .vertical: + itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize) + actionOriginY += fullWidthActionSize.height + actionSpacing + case .verticalReversed: + itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize) + actionOriginY -= fullWidthActionSize.height + actionSpacing + } + itemView.applySize(size: itemFrame.size, transition: transition) + transition.setFrame(view: itemView, frame: itemFrame) + } + + var removeActionIds: [AnyHashable] = [] + for (id, item) in self.actions { + if !validActionIds.contains(id) { + removeActionIds.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 removeActionIds { + self.actions.removeValue(forKey: id) + } + + let alertSize = CGSize(width: alertWidth, height: alertHeight) + let bounds = CGRect(origin: .zero, size: availableSize) + + transition.setFrame(view: self.dimView, frame: bounds) + transition.setFrame(view: self.containerView, frame: bounds) + self.containerView.update(size: availableSize, isDark: environment.theme.overallDarkAppearance, transition: transition) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - alertSize.height) / 2.0)), size: alertSize)) + self.backgroundView.update(size: alertSize, shape: .roundedRect(cornerRadius: 35.0), isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: .white), isInteractive: true, transition: transition) + + 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 func componentAlertController(theme: AlertControllerTheme, content: AnyComponent, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout = .horizontal, dismissOnOutsideTap: Bool = true) -> AlertController { - var dismissImpl: (() -> Void)? - let controller = AlertController(theme: theme, contentNode: ComponentAlertContentNode(theme: theme, content: content, actions: actions.map { action in - return ComponentAlertAction(type: action.type, title: action.title, action: { - dismissImpl?() - action.action() - }) - }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap)) - dismissImpl = { [weak controller] in - controller?.dismissAnimated() +public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { + public enum ActionAligmnent: Equatable { + case `default` + case vertical + } + + public struct Configuration: Equatable { + let actionAlignment: ActionAligmnent + let dismissOnOutsideTap: Bool + let allowInputInset: Bool + + public init( + actionAlignment: ActionAligmnent, + dismissOnOutsideTap: Bool, + allowInputInset: Bool + ) { + self.actionAlignment = actionAlignment + self.dismissOnOutsideTap = dismissOnOutsideTap + self.allowInputInset = allowInputInset + } + + public static var defaultValue: Configuration { + return Configuration( + actionAlignment: .default, + dismissOnOutsideTap: true, + allowInputInset: false + ) + } + } + + public struct Action: Equatable { + public enum ActionType: Equatable { + case generic + case `default` + case destructive + case defaultDestructive + } + public let title: String + public let type: ActionType + public let action: () -> Void + public let autoDismiss: Bool + public let progressPromise: ValuePromise + + public init( + title: String, + type: ActionType = .generic, + action: @escaping () -> Void = {}, + autoDismiss: Bool = true, + progressPromise: ValuePromise = ValuePromise(false) + ) { + self.type = type + self.title = title + self.action = action + self.autoDismiss = autoDismiss + self.progressPromise = progressPromise + } + + public static func ==(lhs: Action, rhs: Action) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.type != rhs.type { + return false + } + if lhs.autoDismiss != rhs.autoDismiss { + return false + } + return true + } + + fileprivate let id: Int64 = Int64.random(in: Int64.min ..< Int64.max) + } + + private var processedDidAppear: Bool = false + private var processedDidDisappear: Bool = false + + private let readyValue = Promise(true) + override public var ready: Promise { + return self.readyValue + } + + public var dismissed: ((Bool) -> Void)? + + public init( + configuration: Configuration = .defaultValue, + content: [AnyComponentWithIdentity], + actions: [Action], + updatedPresentationData: (initial: PresentationData, signal: Signal) + ) { + let componentReady = Promise() + + super.init( + component: AlertScreenComponent( + configuration: configuration, + content: content, + actions: actions, + ready: componentReady + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + presentationMode: .default, + updatedPresentationData: updatedPresentationData + ) + self.navigationPresentation = .flatModal + + //self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true))) + } + + public convenience init( + context: AccountContext, + configuration: Configuration = .defaultValue, + content: [AnyComponentWithIdentity], + actions: [Action] + ) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let updatedPresentationDataSignal = context.sharedContext.presentationData + self.init( + configuration: configuration, + content: content, + actions: actions, + updatedPresentationData: (initial: presentationData, signal: updatedPresentationDataSignal) + ) + } + + public convenience init( + configuration: Configuration = .defaultValue, + title: String? = nil, + text: String, + actions: [Action], + updatedPresentationData: (initial: PresentationData, signal: Signal) + ) { + var content: [AnyComponentWithIdentity] = [] + if let title { + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + )) + } + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + + self.init( + configuration: configuration, + content: content, + actions: actions, + updatedPresentationData: updatedPresentationData + ) + } + + public convenience init( + context: AccountContext, + configuration: Configuration = .defaultValue, + title: String? = nil, + text: String, + actions: [Action] + ) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let updatedPresentationDataSignal = context.sharedContext.presentationData + self.init( + configuration: configuration, + title: title, + text: text, + actions: actions, + updatedPresentationData: (initial: presentationData, signal: updatedPresentationDataSignal) + ) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.processedDidAppear { + self.processedDidAppear = true + if let componentView = self.node.hostView.componentView as? AlertScreenComponent.View { + componentView.animateIn() + } + } + } + + private func superDismiss() { + super.dismiss() + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.processedDidDisappear { + self.processedDidDisappear = true + + if let componentView = self.node.hostView.componentView as? AlertScreenComponent.View { + let dismissedByTapOutside = componentView.dismissedByTapOutside + componentView.animateOut(completion: { [weak self] in + if let self { + self.dismissed?(dismissedByTapOutside) + self.superDismiss() + } + completion?() + }) + } else { + super.dismiss(completion: completion) + } + } + } + + public var keyShortcuts: [KeyShortcut] { + return [ + KeyShortcut( + input: UIKeyCommand.inputEscape, + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View { + componentView.handleKeyCommand(.escape) + } + } + ), + KeyShortcut( + input: "W", + modifiers: [.command], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View { + componentView.handleKeyCommand(.escape) + } + } + ), + KeyShortcut( + input: "\r", + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View { + componentView.handleKeyCommand(.enter) + } + } + ), + KeyShortcut( + input: UIKeyCommand.inputUpArrow, + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View { + componentView.handleKeyCommand(.up) + } + } + ), + KeyShortcut( + input: UIKeyCommand.inputDownArrow, + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View { + componentView.handleKeyCommand(.down) + } + } + ), + KeyShortcut( + input: UIKeyCommand.inputLeftArrow, + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View { + componentView.handleKeyCommand(.left) + } + } + ), + KeyShortcut( + input: UIKeyCommand.inputRightArrow, + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View { + componentView.handleKeyCommand(.right) + } + } + ) + ] + } +} + +public final class ActionSelectionGestureRecognizer: UIGestureRecognizer { + private var initialLocation: CGPoint? + private var currentLocation: CGPoint? + + public override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.delaysTouchesBegan = false + self.delaysTouchesEnded = false + } + + public override func reset() { + super.reset() + + self.initialLocation = nil + } + + public override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.initialLocation == nil { + self.initialLocation = touches.first?.location(in: self.view) + } + self.currentLocation = self.initialLocation + + self.state = .began + } + + public override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.state = .ended + } + + public override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.state = .cancelled + } + + public override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + self.currentLocation = touches.first?.location(in: self.view) + + self.state = .changed + } + + public func translation(in: UIView?) -> CGPoint { + if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation { + return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y) + } + return CGPoint() } - return controller } diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift new file mode 100644 index 0000000000..c008872637 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift @@ -0,0 +1,218 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import TelegramPresentationData +import MultilineTextComponent +import Markdown +import TextFormat + +private let titleFont = Font.bold(17.0) +private let defaultTextFont = Font.regular(15.0) +private let defaultBoldTextFont = Font.semibold(15.0) +private let smallTextFont = Font.regular(14.0) +private let smallBoldTextFont = Font.semibold(14.0) + +public final class AlertTitleComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + let title: String + + public init( + title: String + ) { + self.title = title + } + + public static func ==(lhs: AlertTitleComponent, rhs: AlertTitleComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + return true + } + + public final class View: UIView { + private let title = ComponentView() + + private var component: AlertTitleComponent? + private weak var state: EmptyComponentState? + + func update(component: AlertTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: titleFont, + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: .zero, size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + return CGSize(width: availableSize.width, height: titleSize.height) + } + } + + 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) + } +} + +public final class AlertTextComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public enum Content: Equatable { + case plain(String) + case attributed(NSAttributedString) + } + + public enum Color { + case primary + case secondary + case destructive + } + + public enum TextSize { + case `default` + case small + } + + let content: Content + let color: Color + let textSize: TextSize + let action: ([NSAttributedString.Key: Any]) -> Void + + public init( + content: Content, + color: Color = .primary, + textSize: TextSize = .default, + action: @escaping ([NSAttributedString.Key: Any]) -> Void = { _ in } + ) { + self.content = content + self.color = color + self.textSize = textSize + self.action = action + } + + public static func ==(lhs: AlertTextComponent, rhs: AlertTextComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.textSize != rhs.textSize { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: UIView { + private let text = ComponentView() + + private var component: AlertTextComponent? + private weak var state: EmptyComponentState? + + func update(component: AlertTextComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let textColor: UIColor + switch component.color { + case .primary: + textColor = environment.theme.actionSheet.primaryTextColor + case .secondary: + textColor = environment.theme.actionSheet.secondaryTextColor + case .destructive: + textColor = environment.theme.actionSheet.destructiveActionTextColor + } + let linkColor = environment.theme.actionSheet.controlAccentColor + + let textFont: UIFont + let boldTextFont: UIFont + switch component.textSize { + case .default: + textFont = defaultTextFont + boldTextFont = defaultBoldTextFont + case .small: + textFont = smallTextFont + boldTextFont = smallBoldTextFont + } + + var finalText: NSAttributedString + switch component.content { + case let .plain(text): + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + finalText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes) + case let .attributed(attributedText): + finalText = attributedText + } + + let textSize = self.text.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(finalText), + maximumNumberOfLines: 0, + lineSpacing: 0.1, + highlightColor: linkColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + component.action(attributes) + } + )), + environment: {}, + containerSize: availableSize + ) + let textFrame = CGRect(origin: .zero, size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + transition.setFrame(view: textView, frame: textFrame) + } + return CGSize(width: availableSize.width, height: textSize.height) + } + } + + 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/AlertComponent/Sources/LegacyAlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/LegacyAlertComponent.swift new file mode 100644 index 0000000000..d7b9b0668f --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/LegacyAlertComponent.swift @@ -0,0 +1,395 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import AsyncDisplayKit + +private let alertWidth: CGFloat = 270.0 + +public enum ComponentAlertActionType { + case genericAction + case defaultAction + case destructiveAction + case defaultDestructiveAction +} + +public struct ComponentAlertAction { + public let type: ComponentAlertActionType + public let title: String + public let action: () -> Void + + public init(type: ComponentAlertActionType, title: String, action: @escaping () -> Void) { + self.type = type + self.title = title + self.action = action + } +} + +public final class ComponentAlertContentActionNode: HighlightableButtonNode { + private var theme: AlertControllerTheme + public var action: ComponentAlertAction { + didSet { + self.updateTitle() + } + } + + private let backgroundNode: ASDisplayNode + + public var highlightedUpdated: (Bool) -> Void = { _ in } + + public init(theme: AlertControllerTheme, action: ComponentAlertAction) { + self.theme = theme + self.action = action + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.alpha = 0.0 + + super.init() + + self.titleNode.maximumNumberOfLines = 2 + + self.highligthedChanged = { [weak self] value in + if let strongSelf = self { + strongSelf.setHighlighted(value, animated: true) + } + } + + self.updateTheme(theme) + } + + public override func didLoad() { + super.didLoad() + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + + self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in + if let strongSelf = self { + strongSelf.setHighlighted(true, animated: false) + } + }, willExit: { [weak self] in + if let strongSelf = self { + strongSelf.setHighlighted(false, animated: false) + } + }) + } + + public func performAction() { + if self.actionEnabled { + self.action.action() + } + } + + public func setHighlighted(_ highlighted: Bool, animated: Bool) { + self.highlightedUpdated(highlighted) + if highlighted { + if self.backgroundNode.supernode == nil { + self.insertSubnode(self.backgroundNode, at: 0) + } + self.backgroundNode.alpha = 1.0 + } else { + if animated { + UIView.animate(withDuration: 0.3, animations: { + self.backgroundNode.alpha = 0.0 + }) + } else { + self.backgroundNode.alpha = 0.0 + } + } + } + public var actionEnabled: Bool = true { + didSet { + self.isUserInteractionEnabled = self.actionEnabled + self.updateTitle() + } + } + + public func updateTheme(_ theme: AlertControllerTheme) { + self.theme = theme + self.backgroundNode.backgroundColor = theme.highlightedItemColor + self.updateTitle() + } + + private func updateTitle() { + var font = Font.regular(theme.baseFontSize) + var color: UIColor + switch self.action.type { + case .defaultAction, .genericAction: + color = self.actionEnabled ? self.theme.accentColor : self.theme.disabledColor + case .destructiveAction, .defaultDestructiveAction: + color = self.actionEnabled ? self.theme.destructiveColor : self.theme.disabledColor + } + switch self.action.type { + case .defaultAction, .defaultDestructiveAction: + font = Font.semibold(theme.baseFontSize) + case .destructiveAction, .genericAction: + break + } + self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: []) + self.accessibilityLabel = self.action.title + self.accessibilityTraits = [.button] + } + + @objc func pressed() { + self.action.action() + } + + override public func layout() { + super.layout() + + self.backgroundNode.frame = self.bounds + } +} + +public enum ComponentAlertContentActionLayout { + case horizontal + case vertical +} + +public final class ComponentAlertContentNode: AlertContentNode { + private var theme: AlertControllerTheme + private let actionLayout: ComponentAlertContentActionLayout + + private let content: AnyComponent + private let contentView = ComponentView() + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [ComponentAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + private let _dismissOnOutsideTap: Bool + override public var dismissOnOutsideTap: Bool { + return self._dismissOnOutsideTap + } + + private var highlightedItemIndex: Int? = nil + + public init(theme: AlertControllerTheme, content: AnyComponent, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout, dismissOnOutsideTap: Bool) { + self.theme = theme + self.actionLayout = actionLayout + self._dismissOnOutsideTap = dismissOnOutsideTap + self.content = content + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isUserInteractionEnabled = false + self.actionNodesSeparator.backgroundColor = theme.separatorColor + + self.actionNodes = actions.map { action -> ComponentAlertContentActionNode in + return ComponentAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + separatorNode.backgroundColor = theme.separatorColor + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.actionNodesSeparator) + + var i = 0 + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + + let index = i + actionNode.highlightedUpdated = { [weak self] highlighted in + if highlighted { + self?.highlightedItemIndex = index + } + } + i += 1 + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + } + + func setHighlightedItemIndex(_ index: Int?, update: Bool = false) { + self.highlightedItemIndex = index + + if update { + var i = 0 + for actionNode in self.actionNodes { + if i == index { + actionNode.setHighlighted(true, animated: false) + } else { + actionNode.setHighlighted(false, animated: false) + } + i += 1 + } + } + } + + override public func decreaseHighlightedIndex() { + let currentHighlightedIndex = self.highlightedItemIndex ?? 0 + + self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true) + } + + override public func increaseHighlightedIndex() { + let currentHighlightedIndex = self.highlightedItemIndex ?? -1 + + self.setHighlightedItemIndex(min(self.actionNodes.count - 1, currentHighlightedIndex + 1), update: true) + } + + override public func performHighlightedAction() { + guard let highlightedItemIndex = self.highlightedItemIndex else { + return + } + + var i = 0 + for itemNode in self.actionNodes { + if i == highlightedItemIndex { + itemNode.performAction() + return + } + i += 1 + } + } + + override public func updateTheme(_ theme: AlertControllerTheme) { + self.theme = theme + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + self.validLayout = size + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var size = size + size.width = min(size.width, alertWidth) + + let contentSize = self.contentView.update( + transition: ComponentTransition(transition), + component: self.content, + environment: {}, + containerSize: CGSize(width: size.width - insets.left - insets.right, height: 10000.0) + ) + + let actionButtonHeight: CGFloat = 44.0 + + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = self.actionLayout + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let resultSize: CGSize + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let contentWidth = alertWidth - insets.left - insets.right + + let contentFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - contentSize.width) / 2.0), y: insets.top), size: contentSize) + if let contentComponentView = self.contentView.view { + if contentComponentView.superview == nil { + self.view.insertSubview(contentComponentView, belowSubview: self.actionNodesSeparator.view) + transition.updateFrame(view: contentComponentView, frame: contentFrame) + } + } + + resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: contentSize.height + actionsHeight + insets.top + insets.bottom) + + self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +public func componentAlertController(theme: AlertControllerTheme, content: AnyComponent, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout = .horizontal, dismissOnOutsideTap: Bool = true) -> AlertController { + var dismissImpl: (() -> Void)? + let controller = AlertController(theme: theme, contentNode: ComponentAlertContentNode(theme: theme, content: content, actions: actions.map { action in + return ComponentAlertAction(type: action.type, title: action.title, action: { + dismissImpl?() + action.action() + }) + }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap)) + dismissImpl = { [weak controller] in + controller?.dismissAnimated() + } + return controller +} diff --git a/submodules/TelegramUI/Components/CameraScreen/BUILD b/submodules/TelegramUI/Components/CameraScreen/BUILD index b169af473a..5d92b5d4c8 100644 --- a/submodules/TelegramUI/Components/CameraScreen/BUILD +++ b/submodules/TelegramUI/Components/CameraScreen/BUILD @@ -95,6 +95,8 @@ swift_library( "//submodules/TelegramCallsUI", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/TelegramUI/Components/LiquidLens", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index b29f30a514..8f05aa0097 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -2097,7 +2097,7 @@ private final class CameraScreenComponent: CombinedComponent { if !isSticker, case .none = component.cameraState.recording, component.cameraState.isStreaming == .none && !state.isTransitioning && hasAllRequiredAccess && component.cameraState.collageProgress < 1.0 - .ulpOfOne { let availableModeControlSize: CGSize if isTablet { - availableModeControlSize = CGSize(width: panelWidth, height: 120.0) + availableModeControlSize = CGSize(width: floor(panelWidth), height: 120.0) } else { availableModeControlSize = availableSize } @@ -2131,7 +2131,6 @@ private final class CameraScreenComponent: CombinedComponent { modeControlPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0 + controlsBottomInset + 16.0) } context.add(modeControl - .clipsToBounds(true) .position(modeControlPosition) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index cfb581ad45..1448df3c0d 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -5,6 +5,8 @@ import ComponentFlow import MultilineTextComponent import TelegramPresentationData import GlassBackgroundComponent +import LiquidLens +import TabSelectionRecognizer extension CameraMode { func title(strings: PresentationStrings) -> String { @@ -70,28 +72,17 @@ final class ModeComponent: Component { final class View: UIView, ComponentTaggedView { private var component: ModeComponent? + private var state: EmptyComponentState? final class ItemView: HighlightTrackingButton { - var pressed: () -> Void = { - - } - init() { super.init(frame: .zero) - - self.isExclusiveTouch = true - - self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) } required init(coder: NSCoder) { preconditionFailure() } - @objc func buttonPressed() { - self.pressed() - } - func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize { let accentColor: UIColor let normalColor: UIColor @@ -113,9 +104,15 @@ final class ModeComponent: Component { } private var backgroundView = UIView() - private var glassContainerView = GlassBackgroundContainerView() - private var selectionView = GlassBackgroundView() - private var itemViews: [Int32: ItemView] = [:] + private var backgroundContainer = GlassBackgroundContainerView() + + private let liquidLensView: LiquidLensView + + private var itemViews: [AnyHashable: ItemView] = [:] + private var selectedItemViews: [AnyHashable: ItemView] = [:] + + private var tabSelectionRecognizer: TabSelectionRecognizer? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? public func matches(tag: Any) -> Bool { if let component = self.component, let componentTag = component.tag { @@ -128,16 +125,23 @@ final class ModeComponent: Component { } init() { + self.liquidLensView = LiquidLensView(useBackgroundContainer: false) + super.init(frame: CGRect()) self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11) self.backgroundView.layer.cornerRadius = 24.0 - + self.layer.allowsGroupOpacity = true self.addSubview(self.backgroundView) - self.backgroundView.addSubview(self.glassContainerView) - self.glassContainerView.contentView.addSubview(self.selectionView) + self.backgroundView.addSubview(self.backgroundContainer) + + self.backgroundContainer.contentView.addSubview(self.liquidLensView) + + let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + self.tabSelectionRecognizer = tabSelectionRecognizer + self.liquidLensView.addGestureRecognizer(tabSelectionRecognizer) } required init?(coder aDecoder: NSCoder) { @@ -162,14 +166,69 @@ final class ModeComponent: Component { override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return self.backgroundView.frame.contains(point) } - - func update(component: ModeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { - self.component = component - let isTablet = component.isTablet - let updatedMode = component.updatedMode + private func item(at point: CGPoint) -> AnyHashable? { + var closestItem: (AnyHashable, CGFloat)? + for (id, itemView) in self.itemViews { + if itemView.frame.contains(point) { + return id + } else { + let distance = abs(point.x - itemView.center.x) + if let closestItemValue = closestItem { + if closestItemValue.1 > distance { + closestItem = (id, distance) + } + } else { + closestItem = (id, distance) + } + } + } + return closestItem?.0 + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let component = self.component else { + return + } + let location = recognizer.location(in: self.liquidLensView.contentView) + switch recognizer.state { + case .began: + if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { + let startX = itemView.frame.minX - 4.0 + self.selectionGestureState = (startX, startX, itemId) + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + case .changed: + if var selectionGestureState = self.selectionGestureState { + selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x + if let itemId = self.item(at: location) { + selectionGestureState.itemId = itemId + } + self.selectionGestureState = selectionGestureState + self.state?.updated(transition: .immediate, isLocal: true) + } + case .ended, .cancelled: + if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state { + guard let item = component.availableModes.first(where: { AnyHashable($0.rawValue) == selectionGestureState.itemId }) else { + return + } + component.updatedMode(item) + } + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + default: + break + } + } + + func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let isTablet = component.isTablet - self.glassContainerView.isHidden = component.isTablet self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11) let inset: CGFloat = 23.0 @@ -180,25 +239,34 @@ final class ModeComponent: Component { var selectedCenter = itemFrame.minX var selectedFrame = itemFrame - var validKeys: Set = Set() + var validKeys: Set = Set() for mode in component.availableModes.reversed() { let id = mode.rawValue validKeys.insert(id) let itemView: ItemView - if let current = self.itemViews[id] { + let selectedItemView: ItemView + if let current = self.itemViews[id], let currentSelected = self.selectedItemViews[id] { itemView = current + selectedItemView = currentSelected } else { itemView = ItemView() - self.backgroundView.addSubview(itemView) + itemView.isUserInteractionEnabled = false self.itemViews[id] = itemView - } - itemView.pressed = { - updatedMode(mode) + self.liquidLensView.contentView.addSubview(itemView) + + selectedItemView = ItemView() + selectedItemView.isUserInteractionEnabled = false + self.selectedItemViews[id] = selectedItemView + self.liquidLensView.selectedContentView.addSubview(selectedItemView) } - let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: mode == component.currentMode, tintColor: component.tintColor) + let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor) itemView.bounds = CGRect(origin: .zero, size: itemSize) + + let _ = selectedItemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: true, tintColor: component.tintColor) + selectedItemView.bounds = CGRect(origin: .zero, size: itemSize) + itemFrame = CGRect(origin: itemFrame.origin, size: itemSize) if mode == component.currentMode { @@ -207,12 +275,14 @@ final class ModeComponent: Component { if isTablet { itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY) + selectedItemView.center = itemView.center if mode == component.currentMode { selectedCenter = itemFrame.midY } itemFrame = itemFrame.offsetBy(dx: 0.0, dy: tabletButtonSize.height + spacing) } else { itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + selectedItemView.center = itemView.center if mode == component.currentMode { selectedCenter = itemFrame.midX } @@ -221,7 +291,7 @@ final class ModeComponent: Component { i += 1 } - var removeKeys: [Int32] = [] + var removeKeys: [AnyHashable] = [] for (id, itemView) in self.itemViews { if !validKeys.contains(id) { removeKeys.append(id) @@ -248,12 +318,19 @@ final class ModeComponent: Component { } let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size) - transition.setFrame(view: self.glassContainerView, frame: containerFrame) + transition.setFrame(view: self.backgroundContainer, frame: containerFrame) - let selectionFrame = selectedFrame.insetBy(dx: -20.0, dy: 3.0) - self.glassContainerView.update(size: containerFrame.size, isDark: true, transition: .immediate) - self.selectionView.update(size: selectionFrame.size, cornerRadius: selectionFrame.height * 0.5, isDark: true, tintColor: .init(kind: .custom, color: UIColor(rgb: 0xffffff, alpha: 0.16)), transition: transition) - transition.setFrame(view: self.selectionView, frame: selectionFrame) + let selectionFrame = selectedFrame.insetBy(dx: -23.0, dy: 3.0) + let lensSelection: (x: CGFloat, width: CGFloat) + if let selectionGestureState = self.selectionGestureState { + lensSelection = (selectionGestureState.currentX, selectionFrame.width) + } else { + lensSelection = (selectionFrame.minX, selectionFrame.width) + } + + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size)) + self.liquidLensView.update(size: containerFrame.size, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: selectionFrame.height), isDark: true, isLifted: self.selectionGestureState != nil, isCollapsed: false, transition: transition) + self.backgroundContainer.update(size: containerFrame.size, isDark: true, transition: .immediate) return size } @@ -264,7 +341,7 @@ final class ModeComponent: Component { } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) + return view.update(component: self, availableSize: availableSize, state: state, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index 80adb8f267..cb3801d027 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -440,6 +440,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { if node.iconNode == nil { let iconNode = ASImageNode() iconNode.contentMode = .center + iconNode.isUserInteractionEnabled = false node.iconNode = iconNode node.addSubnode(iconNode) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD index b5507029f4..30fadb7618 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/WallpaperBackgroundNode", "//submodules/LocalMediaResources", "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", "//submodules/ChatPresentationInterfaceState", "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/ChatControllerInteraction", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 001202a781..21815278ab 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -47,6 +47,7 @@ import ManagedDiceAnimationNode import MessageHaptics import ChatMessageTransitionNode import ChatMessageSuggestedPostInfoNode +import TelegramStringFormatting private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -99,6 +100,11 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var swipeToReplyNode: ChatMessageSwipeToReplyNode? private var swipeToReplyFeedback: HapticFeedback? + private let labelNode: TextNodeWithEntities + private var labelBackgroundNode: WallpaperBubbleBackgroundNode? + private let labelBackgroundMaskNode: ASImageNode + private var cachedMaskLabelBackgroundImage: (CGPoint, UIImage, [CGRect])? + private var selectionNode: ChatMessageSelectionNode? private var deliveryFailedNode: ChatMessageDeliveryFailedNode? private var shareButtonNode: ChatMessageShareButton? @@ -161,6 +167,12 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.textNode.textNode.displaysAsynchronously = false self.textNode.textNode.isUserInteractionEnabled = false + self.labelNode = TextNodeWithEntities() + self.labelNode.textNode.isUserInteractionEnabled = false + self.labelNode.textNode.displaysAsynchronously = false + + self.labelBackgroundMaskNode = ASImageNode() + super.init(rotated: rotated) self.containerNode.shouldBegin = { [weak self] location in @@ -469,9 +481,22 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } else if let telegramDice = self.telegramDice, let diceNode = self.animationNode as? ManagedDiceAnimationNode { if let value = telegramDice.value { + let wasRolling = diceNode.isRolling diceNode.setState(value == 0 ? .rolling : .value(value, true)) + + if wasRolling && !diceNode.isRolling { + Queue.mainQueue().after(3.0, { + self.labelNode.textNode.alpha = 1.0 + self.labelBackgroundNode?.alpha = 1.0 + self.labelNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.labelBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + }) + } } else { diceNode.setState(.rolling) + + self.labelNode.textNode.alpha = 0.0 + self.labelBackgroundNode?.alpha = 0.0 } } else if self.telegramFile == nil && self.telegramDice == nil { let (emoji, fitz) = item.message.text.basicEmoji @@ -816,6 +841,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode) + let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode) + let cachedMaskLabelBackgroundImage = self.cachedMaskLabelBackgroundImage + let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) let viaBotLayout = TextNode.asyncLayout(self.viaBotNode) @@ -850,6 +878,44 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let avatarInset: CGFloat var hasAvatar = false + let labelAttributedText = universalServiceMessageString(presentationData: (item.presentationData.theme.theme, item.presentationData.theme.wallpaper), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true) + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + var labelRects = labelLayout.linesRects() + if labelRects.count > 1 { + let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) + for i in 0 ..< sortedIndices.count { + let index = sortedIndices[i] + for j in -1 ... 1 { + if j != 0 && index + j >= 0 && index + j < sortedIndices.count { + if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { + labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) + labelRects[index].size.width = labelRects[index + j].size.width + } + } + } + } + } + for i in 0 ..< labelRects.count { + labelRects[i] = labelRects[i].insetBy(dx: -7.0, dy: floor((labelRects[i].height - 22.0) / 2.0)) + labelRects[i].size.height = 22.0 + labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) + } + + let backgroundMaskImage: (CGPoint, UIImage)? + var backgroundMaskUpdated = false + if labelLayout.size.height > 0.0 { + if let (currentOffset, currentImage, currentRects) = cachedMaskLabelBackgroundImage, currentRects == labelRects { + backgroundMaskImage = (currentOffset, currentImage) + } else { + backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 11.0, outerRadius: 11.0, rects: labelRects, useModernPathCalculation: false) + backgroundMaskUpdated = true + } + } else { + backgroundMaskImage = nil + } + switch item.chatLocation { case let .peer(peerId): if peerId != item.context.account.peerId { @@ -1493,6 +1559,51 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + + let _ = labelApply(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 labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size) + strongSelf.labelNode.textNode.frame = labelFrame + if strongSelf.labelNode.textNode.supernode == nil, labelLayout.size.height > 0.0 { + strongSelf.addSubnode(strongSelf.labelNode.textNode) + } + + let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + if let (offset, image) = backgroundMaskImage { + if strongSelf.labelBackgroundNode == nil { + if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundNode.alpha = strongSelf.labelNode.textNode.alpha + strongSelf.labelBackgroundNode = backgroundNode + strongSelf.insertSubnode(backgroundNode, at: 0) + } + } + + if backgroundMaskUpdated, let backgroundNode = strongSelf.labelBackgroundNode { + if labelRects.count == 1 { + backgroundNode.clipsToBounds = true + backgroundNode.cornerRadius = labelRects[0].height / 2.0 + backgroundNode.view.mask = nil + } else { + backgroundNode.clipsToBounds = false + backgroundNode.cornerRadius = 0.0 + backgroundNode.view.mask = strongSelf.labelBackgroundMaskNode.view + } + } + + if let backgroundNode = strongSelf.labelBackgroundNode { + backgroundNode.layer.frame = CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size) + } + strongSelf.labelBackgroundMaskNode.image = image + strongSelf.labelBackgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size) + + strongSelf.cachedMaskLabelBackgroundImage = (offset, image, labelRects) + } + var updatedImageFrame: CGRect var contextContentFrame: CGRect if let _ = emojiString { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 730e74f103..b19d721b66 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -455,7 +455,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { switch action.action { case let .giftPremium(_, _, daysValue, _, _, giftText, giftEntities): months = max(3, Int32(round(Float(daysValue) / 30.0))) - if months == 12 { + if daysValue < 30 { + title = item.presentationData.strings.Notification_PremiumGift_DaysTitle(daysValue) + } else if months == 12 { title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1) } else { title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months) @@ -513,7 +515,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { title = item.presentationData.strings.Notification_StarsGiveaway_Title let starsString = item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(clamping: count)).replacingOccurrences(of: " ", with: "\u{00A0}") text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, starsString).string - case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities): + case let .giftCode(_, fromGiveaway, unclaimed, channelId, daysValue, _, _, _, _, giftText, giftEntities): + let monthsValue = max(3, Int32(round(Float(daysValue) / 30.0))) if channelId == nil { months = monthsValue if months == 12 { diff --git a/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift index 7f38fa1efb..d6e4efc807 100644 --- a/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift @@ -100,7 +100,7 @@ public struct InteractiveEmojiConfiguration { } public static func with(appConfiguration: AppConfiguration) -> InteractiveEmojiConfiguration { - if let data = appConfiguration.data, let emojis = data["emojies_send_dice"] as? [String] { + if let data = appConfiguration.data, var emojis = data["emojies_send_dice"] as? [String] { var successParameters: [String: InteractiveEmojiSuccessParameters] = [:] if let success = data["emojies_send_dice_success"] as? [String: [String: Double]] { for (key, dict) in success { @@ -109,6 +109,11 @@ public struct InteractiveEmojiConfiguration { } } } + #if DEBUG + if !emojis.contains("🎲") { + emojis.append("🎲") + } + #endif return InteractiveEmojiConfiguration(emojis: emojis, successParameters: successParameters) } else { return .defaultValue @@ -126,6 +131,10 @@ public final class ManagedDiceAnimationNode: ManagedAnimationNode { private let configuration = Promise() private let emojis = Promise<[TelegramMediaFile]>() + public var isRolling: Bool { + return self.diceState == .rolling + } + public var success: (() -> Void)? public init(context: AccountContext, emoji: String) { diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 3daaeff1aa..efac328858 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -33,6 +33,8 @@ import LegacyMessageInputPanelInputView import AttachmentTextInputPanelNode import GlassBackgroundComponent +private let keyboardCornerRadius: CGFloat = 30.0 + public final class EmptyInputView: UIView, UIInputViewAudioFeedback { public var enableInputClicksWhenVisible: Bool { return true @@ -490,7 +492,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.clippingView = UIView() self.clippingView.clipsToBounds = true - self.clippingView.layer.cornerRadius = 20.0 + self.clippingView.layer.cornerRadius = keyboardCornerRadius self.entityKeyboardView = ComponentHostView() @@ -1904,8 +1906,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { theme: interfaceState.theme, strings: interfaceState.strings, isContentInFocus: isVisible, - containerInsets: UIEdgeInsets(top: self.isEmojiSearchActive ? -34.0 : 0.0, left: leftInset, bottom: keyboardBottomInset, right: rightInset), - topPanelInsets: UIEdgeInsets(), + containerInsets: UIEdgeInsets(top: self.isEmojiSearchActive ? -42.0 : 0.0, left: leftInset, bottom: keyboardBottomInset, right: rightInset), + topPanelInsets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 5.0), emojiContent: emojiContent, stickerContent: stickerContent, maskContent: nil, @@ -2030,16 +2032,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { backgroundFrame.size.height += 32.0 if backgroundChromeView.image == nil { - backgroundChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 20.0 * 2.0, height: 20.0 * 2.0), isDark: interfaceState.theme.overallDarkAppearance, fillColor: .clear) + backgroundChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: keyboardCornerRadius * 2.0, height: keyboardCornerRadius * 2.0), isDark: interfaceState.theme.overallDarkAppearance, fillColor: .clear) } if backgroundTintView.image == nil { - backgroundTintView.image = generateStretchableFilledCircleImage(diameter: 20.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + backgroundTintView.image = generateStretchableFilledCircleImage(diameter: keyboardCornerRadius * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) } backgroundTintView.tintColor = interfaceState.theme.chat.inputMediaPanel.backgroundColor transition.updateFrame(view: backgroundView, frame: backgroundFrame) backgroundView.updateColor(color: .clear, forceKeepBlur: true, transition: .immediate) - backgroundView.update(size: backgroundFrame.size, cornerRadius: 20.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition) + backgroundView.update(size: backgroundFrame.size, cornerRadius: keyboardCornerRadius, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition) transition.updateFrame(view: backgroundChromeView, frame: backgroundFrame.insetBy(dx: -1.0, dy: 0.0)) diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD b/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD new file mode 100644 index 0000000000..0200885e73 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD @@ -0,0 +1,55 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EmojiGameStakeScreen", + module_name = "EmojiGameStakeScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ItemListUI", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SheetComponent", + "//submodules/UndoUI", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ScrollComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/PasswordSetupUI", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/ChatScheduleTimeController", + "//submodules/TelegramUI/Components/TabSelectorComponent", + "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", + "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift new file mode 100644 index 0000000000..49e1b27f26 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift @@ -0,0 +1,2187 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import BundleIconComponent +import ButtonComponent +import AccountContext +import PresentationDataUtils +import ListSectionComponent +import TelegramStringFormatting +import UndoUI +import ListActionItemComponent +import PresentationDataUtils +import BalanceNeededScreen +import GlassBarButtonComponent +import GlassBackgroundComponent +import StarsBalanceOverlayComponent +import LottieComponent +import LottieComponentResourceContent +import EdgeEffect +import PlainButtonComponent + +private let amountTag = GenericComponentViewTag() + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let gameInfo: EmojiGameInfo.Info + let controller: () -> ViewController? + let dismiss: () -> Void + + init( + context: AccountContext, + gameInfo: EmojiGameInfo.Info, + controller: @escaping () -> ViewController?, + dismiss: @escaping () -> Void + ) { + self.context = context + self.gameInfo = gameInfo + self.controller = controller + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + return true + } + + static var body: (CombinedComponentContext) -> CGSize { + let description = Child(BalancedTextComponent.self) + let resultsTitle = Child(MultilineTextComponent.self) + let results = Child(VStack.self) + let resultsFooter = Child(MultilineTextWithEntitiesComponent.self) + let amountSection = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) + + let body: (CombinedComponentContext) -> CGSize = { (context: CombinedComponentContext) -> CGSize in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + state.component = component + + let controller = environment.controller + + let theme = environment.theme.withModalBlocksBackground() + //let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 32.0 + environment.safeInsets.left + var contentSize = CGSize(width: context.availableSize.width, height: 75.0) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = theme.actionSheet.primaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + //TODO:localize + let description = description.update( + component: BalancedTextComponent( + text: .markdown(text: "An experimental feature for Telegram Premium users.", attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + description.size.height * 0.5)) + ) + contentSize.height += description.size.height + contentSize.height += 32.0 + + let resultsTitle = resultsTitle.update( + component: MultilineTextComponent(text: .plain(NSAttributedString( + string: "RESULTS AND RETURNS".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + ))), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(resultsTitle + .position(CGPoint(x: textSideInset + resultsTitle.size.width * 0.5, y: contentSize.height + resultsTitle.size.height * 0.5)) + ) + contentSize.height += resultsTitle.size.height + contentSize.height += 6.0 + + let resultSpacing: CGFloat = 8.0 + let resultSize = CGSize(width: (context.availableSize.width - sideInset * 2.0 - resultSpacing * 3.0) / 4.0, height: 64.0) + let doubleResultSize = CGSize(width: resultSize.width * 2.0 + resultSpacing, height: resultSize.height) + + var resultValue1: Int32 = 0 + var resultValue2: Int32 = 300 + var resultValue3: Int32 = 600 + var resultValue4: Int32 = 1300 + var resultValue5: Int32 = 1600 + var resultValue6: Int32 = 2000 + var resultValue7: Int32 = 20000 + if context.component.gameInfo.parameters.count == 7 { + resultValue1 = context.component.gameInfo.parameters[0] + resultValue2 = context.component.gameInfo.parameters[1] + resultValue3 = context.component.gameInfo.parameters[2] + resultValue4 = context.component.gameInfo.parameters[3] + resultValue5 = context.component.gameInfo.parameters[4] + resultValue6 = context.component.gameInfo.parameters[5] + resultValue7 = context.component.gameInfo.parameters[6] + } + + let results = results.update( + component: VStack([ + AnyComponentWithIdentity(id: "first", component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: 1, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[1]] }, value: resultValue1, size: resultSize) + )), + AnyComponentWithIdentity(id: 2, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[2]] }, value: resultValue2, size: resultSize) + )), + AnyComponentWithIdentity(id: 3, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[3]] }, value: resultValue3, size: resultSize) + )), + AnyComponentWithIdentity(id: 4, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[4]] }, value: resultValue4, size: resultSize) + )) + ], spacing: resultSpacing) + )), + AnyComponentWithIdentity(id: "second", component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: 5, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[5]] }, value: resultValue5, size: resultSize) + )), + AnyComponentWithIdentity(id: 6, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[6]] }, value: resultValue6, size: resultSize) + )), + AnyComponentWithIdentity(id: 7, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[6], $0[6], $0[6]] }, value: resultValue7, size: doubleResultSize) + )), + ], spacing: resultSpacing) + )) + ], spacing: resultSpacing), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(results + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + results.size.height * 0.5)) + ) + contentSize.height += results.size.height + contentSize.height += 7.0 + + let resultsFooterAttributedText = NSMutableAttributedString( + string: "A streak resets after 3 # or a stake change.", + font: Font.regular(13.0), + textColor: theme.list.freeTextColor + ) + if let emojiFile = state.emojiFiles?[6] { + let range = (resultsFooterAttributedText.string as NSString).range(of: "#") + if range.location != NSNotFound { + resultsFooterAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), range: range) + } + } + + let resultsFooter = resultsFooter.update( + component: MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .clear, + text: .plain(resultsFooterAttributedText), + maximumNumberOfLines: 0, + enableLooping: true + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(resultsFooter + .position(CGPoint(x: textSideInset + resultsFooter.size.width * 0.5, y: contentSize.height + resultsFooter.size.height * 0.5)) + ) + contentSize.height += resultsFooter.size.height + contentSize.height += 39.0 + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } + + let configuration = EmojiGameStakeConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + var amountLabel = "" + if let tonUsdRate = configuration.tonUsdRate, let value = state.amount?.value, value > 0 { + amountLabel = "~\(formatTonUsdValue(value, divide: true, rate: tonUsdRate, dateTimeFormat: environment.dateTimeFormat))" + } + + let amountItems: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity( + id: "amount", + component: AnyComponent( + AmountFieldComponent( + textColor: theme.list.itemPrimaryTextColor, + secondaryColor: theme.list.itemSecondaryTextColor, + placeholderColor: theme.list.itemPlaceholderTextColor, + accentColor: theme.list.itemAccentColor, + value: state.amount?.value, + minValue: 0, + forceMinValue: false, + allowZero: true, + maxValue: nil, + placeholderText: "Amount", + labelText: amountLabel, + currency: .ton, + dateTimeFormat: presentationData.dateTimeFormat, + amountUpdated: { [weak state] amount in + state?.amount = amount.flatMap { StarsAmount(value: $0, nanos: 0) } + state?.updated() + }, + tag: amountTag + ) + ) + ), + AnyComponentWithIdentity(id: "presets", component: AnyComponent( + AmountPresetsListItemComponent( + context: component.context, + theme: theme, + values: [ + 100000000, + 1000000000, + 2000000000, + 5000000000, + 10000000000, + 20000000000 + ], + valueSelected: { [weak state] value in + guard let state else { + return + } + state.amount = StarsAmount(value: value, nanos: 0) + if let controller = controller() as? EmojiGameStakeScreen { + controller.dismissInput() + state.updated() + controller.resetValue() + } + } + ) + )) + ] + + //TODO:localize + let amountSection = amountSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "STAKE".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: amountItems + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: .immediate + ) + context.add(amountSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += amountSection.size.height + contentSize.height += 24.0 + + + //TODO:localize + var buttonItems: [AnyComponentWithIdentity] = [] + buttonItems.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Premium/Dice", tintColor: theme.list.itemCheckColors.foregroundColor)))) + buttonItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Save and Roll", font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor)))) + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + 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) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonItems, spacing: 7.0)) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak state] in + if let state, let amount = state.amount, let controller = controller() as? EmojiGameStakeScreen { + controller.complete(amount: amount) + } + } + ), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), + transition: .immediate + ) + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + + if environment.inputHeight > 0.0 { + contentSize.height += 15.0 + contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) + } else { + contentSize.height += buttonInsets.bottom + } + + return contentSize + } + + return body + } + + final class State: ComponentState { + fileprivate let context: AccountContext + + fileprivate var component: SheetContent + + fileprivate var forceUpdateAmount = false + fileprivate var amount: StarsAmount? + fileprivate var currency: CurrencyAmount.Currency = .ton + + var cachedChevronImage: (UIImage, PresentationTheme)? + + var emojiFiles: [TelegramMediaFile]? + var emojiFilesDisposable: Disposable? + + init(component: SheetContent) { + self.context = component.context + self.component = component + + let amount: StarsAmount? = StarsAmount(value: component.gameInfo.previousStake, nanos: 0) + let currency: CurrencyAmount.Currency = .ton + + self.currency = currency + self.amount = amount + + super.init() + + self.emojiFilesDisposable = (self.context.engine.stickers.loadedStickerPack(reference: .dice("🎲"), forceActualized: false) + |> mapToSignal { stickerPack -> Signal<[TelegramMediaFile], NoError> in + switch stickerPack { + case let .result(_, items, _): + var emojiStickers: [TelegramMediaFile] = [] + for item in items { + emojiStickers.append(item.file._parse()) + } + return .single(emojiStickers) + default: + return .complete() + } + } + |> deliverOnMainQueue).start(next: { [weak self] files in + guard let self else { + return + } + self.emojiFiles = files + self.updated() + }) + } + + deinit { + self.emojiFilesDisposable?.dispose() + } + } + + func makeState() -> State { + return State(component: self) + } +} + +private final class EmojiGameStakeSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let gameInfo: EmojiGameInfo.Info + + init( + context: AccountContext, + gameInfo: EmojiGameInfo.Info + ) { + self.context = context + self.gameInfo = gameInfo + } + + static func ==(lhs: EmojiGameStakeSheetComponent, rhs: EmojiGameStakeSheetComponent) -> Bool { + return true + } + + static var body: Body { + let sheet = Child(ResizableSheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let dismiss: (Bool) -> Void = { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + + let theme = environment.theme.withModalBlocksBackground() + + var buttonItems: [AnyComponentWithIdentity] = [] + buttonItems.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(Image(image: PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme), tintColor: theme.list.itemCheckColors.foregroundColor, size: CGSize(width: 16.0, height: 18.0))))) + buttonItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Save and Roll", font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor)))) + + let sheet = sheet.update( + component: ResizableSheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + gameInfo: context.component.gameInfo, + controller: { + return controller() + }, + dismiss: { + dismiss(true) + } + )), + titleItem: AnyComponent( + Text(text: "Emoji Stake", font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor) + ), + leftItem: AnyComponent( + 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.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + bottomItem: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonItems, spacing: 7.0)) + ), + isEnabled: true, + displaysProgress: false, + action: { + dismiss(true) + } + ) + ), + backgroundColor: .color(theme.list.blocksBackgroundColor), + animateOut: animateOut + ), + environment: { + environment + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class EmojiGameStakeScreen: ViewControllerComponentContainer { + private let context: AccountContext + fileprivate let completion: (StarsAmount) -> Void + + fileprivate let balanceOverlay = ComponentView() + private var showBalance = true + + public init( + context: AccountContext, + gameInfo: EmojiGameInfo.Info, + completion: @escaping (StarsAmount) -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: EmojiGameStakeSheetComponent( + context: context, + gameInfo: gameInfo + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + + self.context.tonContext?.load(force: true) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func dismissInput() { + if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View { + view.deactivateInput() + } + } + + fileprivate func resetValue() { + if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View { + view.resetValue() + } + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } + + func complete(amount: StarsAmount) { + if let tonState = self.context.tonContext?.currentState, tonState.balance < amount { + let needed = amount - tonState.balance + var fragmentUrl = "https://fragment.com/ads/topup" + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String { + fragmentUrl = value + } + self.push(BalanceNeededScreen( + context: self.context, + amount: needed, + buttonAction: { [weak self] in + self?.context.sharedContext.applicationBindings.openUrl(fragmentUrl) + } + )) + } else { + self.completion(amount) + self.dismissAnimated() + } + } + + func dismissBalanceOverlay() { + if let view = self.balanceOverlay.view, view.superview != nil { + view.alpha = 0.0 + view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4) + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { _ in + view.removeFromSuperview() + view.alpha = 1.0 + }) + } + } + + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if self.showBalance { + let context = self.context + let insets = layout.insets(options: .statusBar) + let balanceSize = self.balanceOverlay.update( + transition: .immediate, + component: AnyComponent( + StarsBalanceOverlayComponent( + context: context, + peerId: context.account.peerId, + theme: context.sharedContext.currentPresentationData.with { $0 }.theme, + currency: .ton, + action: { [weak self] in + guard let self else { + return + } + var fragmentUrl = "https://fragment.com/ads/topup" + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String { + fragmentUrl = value + } + context.sharedContext.applicationBindings.openUrl(fragmentUrl) + + self.dismissAnimated() + } + ) + ), + environment: {}, + containerSize: layout.size + ) + if let view = self.balanceOverlay.view { + if view.superview == nil { + self.view.addSubview(view) + + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + view.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - balanceSize.width) / 2.0), y: insets.top + 5.0), size: balanceSize) + } + } else if let view = self.balanceOverlay.view, view.superview != nil { + view.alpha = 0.0 + view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4) + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { _ in + view.removeFromSuperview() + view.alpha = 1.0 + }) + } + } +} + +private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { + private let currency: CurrencyAmount.Currency + private let dateTimeFormat: PresentationDateTimeFormat + + private let textField: UITextField + private let minValue: Int64 + private let forceMinValue: Bool + private let allowZero: Bool + private let maxValue: Int64 + private let updated: (Int64) -> Void + private let isEmptyUpdated: (Bool) -> Void + private let animateError: () -> Void + private let focusUpdated: (Bool) -> Void + + init?(textField: UITextField, currency: CurrencyAmount.Currency, dateTimeFormat: PresentationDateTimeFormat, minValue: Int64, forceMinValue: Bool, allowZero: Bool, maxValue: Int64, updated: @escaping (Int64) -> Void, isEmptyUpdated: @escaping (Bool) -> Void, animateError: @escaping () -> Void, focusUpdated: @escaping (Bool) -> Void) { + self.textField = textField + self.currency = currency + self.dateTimeFormat = dateTimeFormat + self.minValue = minValue + self.forceMinValue = forceMinValue + self.allowZero = allowZero + self.maxValue = maxValue + self.updated = updated + self.isEmptyUpdated = isEmptyUpdated + self.animateError = animateError + self.focusUpdated = focusUpdated + + super.init() + } + + func amountFrom(text: String) -> Int64 { + var amount: Int64? + if !text.isEmpty { + switch self.currency { + case .stars: + if let value = Int64(text) { + amount = value + } + case .ton: + let scale: Int64 = 1_000_000_000 // 10⁹ (one “nano”) + if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, let dot = text.firstIndex(of: decimalSeparator) { + // Slices for the parts on each side of the dot + var wholeSlice = String(text[.. 9 { + fractionStr = String(fractionStr.prefix(9)) // trim extra digits + } else { + fractionStr = fractionStr.padding( + toLength: 9, withPad: "0", startingAt: 0) // pad with zeros + } + + // Convert and combine + if let whole = Int64(wholeSlice), + let frac = Int64(fractionStr) { + + let whole = min(whole, Int64.max / scale) + + amount = whole * scale + frac + } + } else if let whole = Int64(text) { // string had no dot at all + let whole = min(whole, Int64.max / scale) + + amount = whole * scale + } + } + } + return amount ?? 0 + } + + func onTextChanged(text: String) { + self.updated(self.amountFrom(text: text)) + self.isEmptyUpdated(text.isEmpty) + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + var acceptZero = false + if case .ton = self.currency, self.minValue < 1_000_000_000 { + acceptZero = true + } + + var newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + if newText.contains(where: { c in + switch c { + case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": + return false + default: + if case .ton = self.currency { + if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, c == decimalSeparator { + return false + } + } + return true + } + }) { + return false + } + if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, newText.count(where: { $0 == decimalSeparator }) > 1 { + return false + } + + switch self.currency { + case .stars: + if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0")) { + newText.removeFirst() + textField.text = newText + self.onTextChanged(text: newText) + return false + } + case .ton: + var fixedText = false + if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, let index = newText.firstIndex(of: decimalSeparator) { + let fractionalString = newText[newText.index(after: index)...] + if fractionalString.count > 2 { + newText = String(newText[newText.startIndex ..< newText.index(index, offsetBy: 3)]) + fixedText = true + } + } + + if newText == self.dateTimeFormat.decimalSeparator { + if !acceptZero { + newText.removeFirst() + } else { + newText = "0\(newText)" + } + fixedText = true + } + + if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0") && !newText.hasPrefix("0\(self.dateTimeFormat.decimalSeparator)")) { + newText.removeFirst() + fixedText = true + } + + if fixedText { + textField.text = newText + self.onTextChanged(text: newText) + return false + } + } + + let amount: Int64 = self.amountFrom(text: newText) + if self.forceMinValue && amount < self.minValue { + switch self.currency { + case .stars: + textField.text = "\(self.minValue)" + case .ton: + textField.text = "\(formatTonAmountText(self.minValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: self.dateTimeFormat.decimalSeparator, groupingSeparator: ""), maxDecimalPositions: nil))" + } + self.onTextChanged(text: self.textField.text ?? "") + self.animateError() + return false + } else if amount > self.maxValue { + switch self.currency { + case .stars: + textField.text = "\(self.maxValue)" + case .ton: + textField.text = "\(formatTonAmountText(self.maxValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: self.dateTimeFormat.decimalSeparator, groupingSeparator: ""), maxDecimalPositions: nil))" + } + self.onTextChanged(text: self.textField.text ?? "") + self.animateError() + return false + } + + self.onTextChanged(text: newText) + + return true + } +} + +public final class AmountFieldComponent: Component { + public typealias EnvironmentType = Empty + + let textColor: UIColor + let secondaryColor: UIColor + let placeholderColor: UIColor + let accentColor: UIColor + let value: Int64? + let minValue: Int64? + let forceMinValue: Bool + let allowZero: Bool + let maxValue: Int64? + let placeholderText: String + let textFieldOffset: CGPoint + let labelText: String? + let currency: CurrencyAmount.Currency + let dateTimeFormat: PresentationDateTimeFormat + let amountUpdated: (Int64?) -> Void + let tag: AnyObject? + + public init( + textColor: UIColor, + secondaryColor: UIColor, + placeholderColor: UIColor, + accentColor: UIColor, + value: Int64?, + minValue: Int64?, + forceMinValue: Bool, + allowZero: Bool, + maxValue: Int64?, + placeholderText: String, + textFieldOffset: CGPoint = .zero, + labelText: String?, + currency: CurrencyAmount.Currency, + dateTimeFormat: PresentationDateTimeFormat, + amountUpdated: @escaping (Int64?) -> Void, + tag: AnyObject? = nil + ) { + self.textColor = textColor + self.secondaryColor = secondaryColor + self.placeholderColor = placeholderColor + self.accentColor = accentColor + self.value = value + self.minValue = minValue + self.forceMinValue = forceMinValue + self.allowZero = allowZero + self.maxValue = maxValue + self.placeholderText = placeholderText + self.textFieldOffset = textFieldOffset + self.labelText = labelText + self.currency = currency + self.dateTimeFormat = dateTimeFormat + self.amountUpdated = amountUpdated + self.tag = tag + } + + public static func ==(lhs: AmountFieldComponent, rhs: AmountFieldComponent) -> Bool { + if lhs.textColor != rhs.textColor { + return false + } + if lhs.secondaryColor != rhs.secondaryColor { + return false + } + if lhs.placeholderColor != rhs.placeholderColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.minValue != rhs.minValue { + return false + } + if lhs.allowZero != rhs.allowZero { + return false + } + if lhs.maxValue != rhs.maxValue { + return false + } + if lhs.placeholderText != rhs.placeholderText { + return false + } + if lhs.labelText != rhs.labelText { + return false + } + if lhs.currency != rhs.currency { + return false + } + return true + } + + public final class View: UIView, ListSectionComponent.ChildView, UITextFieldDelegate, ComponentTaggedView { + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + private let placeholderView: ComponentView + private let icon = ComponentView() + private let textField: TextFieldNodeView + private var starsFormatter: AmountFieldStarsFormatter? + private var tonFormatter: AmountFieldStarsFormatter? + private let labelView: ComponentView + + private var component: AmountFieldComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private var didSetValueOnce = false + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? + public let separatorInset: CGFloat = 16.0 + + public override init(frame: CGRect) { + self.placeholderView = ComponentView() + self.textField = TextFieldNodeView(frame: .zero) + self.labelView = ComponentView() + + super.init(frame: frame) + + self.addSubview(self.textField) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func activateInput() { + self.textField.becomeFirstResponder() + } + + public func deactivateInput() { + self.textField.resignFirstResponder() + } + + public func selectAll() { + self.textField.selectAll(nil) + } + + public func animateError() { + self.textField.layer.addShakeAnimation() + let hapticFeedback = HapticFeedback() + hapticFeedback.error() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { + let _ = hapticFeedback + }) + } + + public func resetValue() { + guard let component = self.component, let value = component.value else { + return + } + var text = "" + switch component.currency { + case .stars: + text = "\(value)" + case .ton: + text = "\(formatTonAmountText(value, dateTimeFormat: PresentationDateTimeFormat(timeFormat: component.dateTimeFormat.timeFormat, dateFormat: component.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), maxDecimalPositions: nil))" + } + self.textField.text = text + self.placeholderView.view?.isHidden = !(self.textField.text ?? "").isEmpty + } + + func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.textField.textColor = component.textColor + if self.component?.currency != component.currency || ((self.textField.text ?? "").isEmpty && !self.didSetValueOnce) { + if let value = component.value, value != .zero { + var text = "" + switch component.currency { + case .stars: + text = "\(value)" + case .ton: + text = "\(formatTonAmountText(value, dateTimeFormat: PresentationDateTimeFormat(timeFormat: component.dateTimeFormat.timeFormat, dateFormat: component.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), maxDecimalPositions: nil))" + } + self.textField.text = text + self.placeholderView.view?.isHidden = !text.isEmpty + } else { + self.textField.text = "" + } + self.didSetValueOnce = true + } + self.textField.font = Font.regular(17.0) + + self.textField.returnKeyType = .done + self.textField.autocorrectionType = .no + self.textField.autocapitalizationType = .none + + if self.component?.currency != component.currency { + switch component.currency { + case .stars: + self.textField.delegate = self + self.textField.keyboardType = .numberPad + if self.starsFormatter == nil { + self.starsFormatter = AmountFieldStarsFormatter( + textField: self.textField, + currency: component.currency, + dateTimeFormat: component.dateTimeFormat, + minValue: component.minValue ?? 0, + forceMinValue: component.forceMinValue, + allowZero: component.allowZero, + maxValue: component.maxValue ?? Int64.max, + updated: { [weak self] value in + guard let self, let component = self.component else { + return + } + if !self.isUpdating { + component.amountUpdated(value == 0 ? nil : value) + } + }, + isEmptyUpdated: { [weak self] isEmpty in + guard let self else { + return + } + self.placeholderView.view?.isHidden = !isEmpty + }, + animateError: { [weak self] in + guard let self else { + return + } + self.animateError() + }, + focusUpdated: { _ in + } + ) + } + self.tonFormatter = nil + self.textField.delegate = self.starsFormatter + case .ton: + self.textField.keyboardType = .decimalPad + if self.tonFormatter == nil { + self.tonFormatter = AmountFieldStarsFormatter( + textField: self.textField, + currency: component.currency, + dateTimeFormat: component.dateTimeFormat, + minValue: component.minValue ?? 0, + forceMinValue: component.forceMinValue, + allowZero: component.allowZero, + maxValue: component.maxValue ?? Int64.max, + updated: { [weak self] value in + guard let self, let component = self.component else { + return + } + if !self.isUpdating { + component.amountUpdated(value == 0 ? nil : value) + } + }, + isEmptyUpdated: { [weak self] isEmpty in + guard let self else { + return + } + self.placeholderView.view?.isHidden = !isEmpty + }, + animateError: { [weak self] in + guard let self else { + return + } + self.animateError() + }, + focusUpdated: { _ in + } + ) + } + self.starsFormatter = nil + self.textField.delegate = self.tonFormatter + } + self.textField.reloadInputViews() + } + + self.component = component + self.state = state + + let size = CGSize(width: availableSize.width, height: 52.0) + + let sideInset: CGFloat = 16.0 + var leftInset: CGFloat = 16.0 + + let iconName: String + var iconTintColor: UIColor? + let iconMaxSize: CGSize? + var iconOffset = CGPoint() + switch component.currency { + case .stars: + iconName = "Premium/Stars/StarLarge" + iconMaxSize = CGSize(width: 22.0, height: 22.0) + case .ton: + iconName = "Ads/TonBig" + iconTintColor = component.accentColor + iconMaxSize = CGSize(width: 18.0, height: 18.0) + iconOffset = CGPoint(x: 3.0, y: 1.0) + } + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: iconName, + tintColor: iconTintColor, + maxSize: iconMaxSize + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = CGRect(origin: CGPoint(x: iconOffset.x + 15.0, y: iconOffset.y - 1.0 + floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) + } + + leftInset += 24.0 + 6.0 + + let placeholderSize = self.placeholderView.update( + transition: .easeInOut(duration: 0.2), + component: AnyComponent( + Text( + text: component.placeholderText, + font: Font.regular(17.0), + color: component.placeholderColor + ) + ), + environment: {}, + containerSize: availableSize + ) + + if let placeholderComponentView = self.placeholderView.view { + if placeholderComponentView.superview == nil { + self.insertSubview(placeholderComponentView, at: 0) + } + + placeholderComponentView.frame = CGRect(origin: CGPoint(x: leftInset, y: -1.0 + floorToScreenPixels((size.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize) + placeholderComponentView.isHidden = !(self.textField.text ?? "").isEmpty + } + + if let labelText = component.labelText { + let labelSize = self.labelView.update( + transition: .immediate, + component: AnyComponent( + Text( + text: labelText, + font: Font.regular(17.0), + color: component.secondaryColor + ) + ), + environment: {}, + containerSize: availableSize + ) + + if let labelView = self.labelView.view { + if labelView.superview == nil { + self.insertSubview(labelView, at: 0) + } + + labelView.frame = CGRect(origin: CGPoint(x: size.width - sideInset - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0) + 1.0 - UIScreenPixel), size: labelSize) + } + } else if let labelView = self.labelView.view, labelView.superview != nil { + labelView.removeFromSuperview() + } + + self.textField.frame = CGRect(x: leftInset + component.textFieldOffset.x, y: 4.0 + component.textFieldOffset.y, width: size.width - 30.0, height: 44.0) + + return size + } + } + + 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 ResultCellComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let files: [TelegramMediaFile]? + let value: Int32 + let size: CGSize + + init( + context: AccountContext, + theme: PresentationTheme, + files: [TelegramMediaFile]?, + value: Int32, + size: CGSize + ) { + self.context = context + self.theme = theme + self.files = files + self.value = value + self.size = size + } + + static func ==(lhs: ResultCellComponent, rhs: ResultCellComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.files != rhs.files { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + + final class View: UIView { + private var component: ResultCellComponent? + + private let background = ComponentView() + private let emoji = ComponentView() + private let title = ComponentView() + + override init(frame: CGRect) { + + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ResultCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let size = component.size + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent(color: component.theme.list.itemBlocksBackgroundColor, cornerRadius: .value(22.0), smoothCorners: true)), + environment: {}, + containerSize: size + ) + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let value = Double(component.value) / 1000.0 + let titleString = String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + + var items: [AnyComponentWithIdentity] = [] + if let files = component.files { + for file in files { + items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent( + LottieComponent( + content: LottieComponent.ResourceContent( + context: component.context, + file: file, + attemptSynchronously: true, + providesPlaceholder: true + ), + placeholderColor: component.theme.list.mediaPlaceholderColor, + startingPosition: .end, + size: CGSize(width: 50.0, height: 50.0), + loop: false + ) + ))) + } + } + let emojiSize = self.emoji.update( + transition: transition, + component: AnyComponent( + HStack(items, spacing: -18.0) + ), + environment: {}, + containerSize: availableSize + ) + let emojiFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - emojiSize.width) / 2.0), y: -9.0), size: emojiSize) + if let emojiView = self.emoji.view { + if emojiView.superview == nil { + self.addSubview(emojiView) + } + transition.setFrame(view: emojiView, frame: emojiFrame) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "×\(titleString)", font: Font.semibold(11.0), textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 10.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + 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 AmountPresetsListItemComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let values: [Int64] + let valueSelected: (Int64) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + values: [Int64], + valueSelected: @escaping (Int64) -> Void + ) { + self.context = context + self.theme = theme + self.values = values + self.valueSelected = valueSelected + } + + static func ==(lhs: AmountPresetsListItemComponent, rhs: AmountPresetsListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.values != rhs.values { + return false + } + return true + } + + final class View: UIView { + private var component: AmountPresetsListItemComponent? + + private var itemViews: [Int64: ComponentView] = [:] + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AmountPresetsListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + + let sideInset: CGFloat = 16.0 + let spacing: CGFloat = 6.0 + let itemSize = CGSize(width: floorToScreenPixels((availableSize.width - sideInset * 2.0 - spacing * 2.0) / 3.0), height: 28.0) + + var itemOrigin = CGPoint(x: sideInset, y: sideInset) + for value in component.values { + let itemView: ComponentView + if let current = self.itemViews[value] { + itemView = current + } else { + itemView = ComponentView() + self.itemViews[value] = itemView + } + let _ = itemView.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(AmountPresetComponent( + context: component.context, + theme: component.theme, + value: value + )), + action: { + component.valueSelected(value) + }, + animateScale: false + )), + environment: {}, + containerSize: itemSize + ) + var itemFrame = CGRect(origin: itemOrigin, size: itemSize) + if itemFrame.maxX > availableSize.width { + itemOrigin = CGPoint(x: sideInset, y: itemOrigin.y + itemSize.height + spacing) + itemFrame.origin = itemOrigin + } + if let itemView = itemView.view { + if itemView.superview == nil { + self.addSubview(itemView) + } + transition.setFrame(view: itemView, frame: itemFrame) + } + itemOrigin.x += itemSize.width + spacing + } + + let size = CGSize(width: availableSize.width, height: itemOrigin.y + itemSize.height + sideInset) + 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 AmountPresetComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let value: Int64 + + init( + context: AccountContext, + theme: PresentationTheme, + value: Int64 + ) { + self.context = context + self.theme = theme + self.value = value + } + + static func ==(lhs: AmountPresetComponent, rhs: AmountPresetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + final class View: UIView { + private var component: AmountPresetComponent? + + private let background = ComponentView() + private let title = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AmountPresetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let size = availableSize + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent(color: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), cornerRadius: .minEdge, smoothCorners: false)), + environment: {}, + containerSize: size + ) + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let attributedText = NSMutableAttributedString(string: "$ \(formatTonAmountText(component.value, dateTimeFormat: presentationData.dateTimeFormat))", font: Font.semibold(14.0), textColor: component.theme.list.itemAccentColor) + if let range = attributedText.string.range(of: "$") { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: true)), range: NSRange(range, in: attributedText.string)) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedText.string)) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .clear, + text: .plain(attributedText) + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + 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 struct EmojiGameStakeConfiguration { + static var defaultValue: EmojiGameStakeConfiguration { + return EmojiGameStakeConfiguration(tonUsdRate: nil) + } + + let tonUsdRate: Double? + + fileprivate init(tonUsdRate: Double?) { + self.tonUsdRate = tonUsdRate + } + + static func with(appConfiguration: AppConfiguration) -> EmojiGameStakeConfiguration { + if let data = appConfiguration.data { + var tonUsdRate: Double? + if let value = data["ton_usd_rate"] as? Double { + tonUsdRate = value + } + + return EmojiGameStakeConfiguration(tonUsdRate: tonUsdRate) + } else { + return .defaultValue + } + } +} + + + + + + + + + + +public final class ResizableSheetComponentEnvironment: Equatable { + public let theme: PresentationTheme + public let statusBarHeight: CGFloat + public let safeInsets: UIEdgeInsets + public let metrics: LayoutMetrics + public let deviceMetrics: DeviceMetrics + public let isDisplaying: Bool + public let isCentered: Bool + public let regularMetricsSize: CGSize? + public let dismiss: (Bool) -> Void + + public init( + theme: PresentationTheme, + statusBarHeight: CGFloat, + safeInsets: UIEdgeInsets, + metrics: LayoutMetrics, + deviceMetrics: DeviceMetrics, + isDisplaying: Bool, + isCentered: Bool, + regularMetricsSize: CGSize?, + dismiss: @escaping (Bool) -> Void + ) { + self.theme = theme + self.statusBarHeight = statusBarHeight + self.safeInsets = safeInsets + self.metrics = metrics + self.deviceMetrics = deviceMetrics + self.isDisplaying = isDisplaying + self.isCentered = isCentered + self.regularMetricsSize = regularMetricsSize + self.dismiss = dismiss + } + + public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool { + if lhs.theme != rhs.theme { + return false + } + if lhs.statusBarHeight != rhs.statusBarHeight { + return false + } + if lhs.safeInsets != rhs.safeInsets { + return false + } + if lhs.metrics != rhs.metrics { + return false + } + if lhs.deviceMetrics != rhs.deviceMetrics { + return false + } + if lhs.isDisplaying != rhs.isDisplaying { + return false + } + if lhs.isCentered != rhs.isCentered { + return false + } + if lhs.regularMetricsSize != rhs.regularMetricsSize { + return false + } + return true + } +} + +public final class ResizableSheetComponent: Component { + public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment) + + public class ExternalState { + public fileprivate(set) var contentHeight: CGFloat + + public init() { + self.contentHeight = 0.0 + } + } + + public enum BackgroundColor: Equatable { + case color(UIColor) + } + + public let content: AnyComponent + public let titleItem: AnyComponent? + public let leftItem: AnyComponent? + public let rightItem: AnyComponent? + public let bottomItem: AnyComponent? + public let backgroundColor: BackgroundColor + public let externalState: ExternalState? + public let animateOut: ActionSlot> + + public init( + content: AnyComponent, + titleItem: AnyComponent? = nil, + leftItem: AnyComponent? = nil, + rightItem: AnyComponent? = nil, + bottomItem: AnyComponent? = nil, + backgroundColor: BackgroundColor, + externalState: ExternalState? = nil, + animateOut: ActionSlot>, + ) { + self.content = content + self.titleItem = titleItem + self.leftItem = leftItem + self.rightItem = rightItem + self.bottomItem = bottomItem + self.backgroundColor = backgroundColor + self.externalState = externalState + self.animateOut = animateOut + } + + public static func ==(lhs: ResizableSheetComponent, rhs: ResizableSheetComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.titleItem != rhs.titleItem { + return false + } + if lhs.leftItem != rhs.leftItem { + return false + } + if lhs.rightItem != rhs.rightItem { + return false + } + if lhs.bottomItem != rhs.bottomItem { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.animateOut != rhs.animateOut { + return false + } + 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 + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { + public final class Tag { + public init() { + } + } + + public func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + + private let dimView: UIView + private let containerView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let topEdgeEffectView: EdgeEffectView + private let contentView: ComponentView + + private var titleItemView: ComponentView? + private var leftItemView: ComponentView? + private var rightItemView: ComponentView? + private var bottomItemView: ComponentView? + + private let backgroundHandleView: UIImageView + + private var ignoreScrolling: Bool = false + + private var component: ResizableSheetComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var environment: ResizableSheetComponentEnvironment? + 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.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + self.topEdgeEffectView = EdgeEffectView() + self.topEdgeEffectView.clipsToBounds = true + self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.topEdgeEffectView.layer.cornerRadius = 40.0 + + self.contentView = ComponentView() + + 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 + self.scrollView.contentInsetAdjustmentBehavior = .never + 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") + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + public 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 { + self.dismissAnimated() + } + } + + func dismissAnimated() { + guard let environment = self.environment else { + return + } + self.endEditing(true) + environment.dismiss(true) + } + + 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) + } + + private var didPlayAppearanceAnimation = false + func animateIn() { + self.didPlayAppearanceAnimation = true + + 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) + } + + func update(component: ResizableSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value + component.animateOut.connect { [weak self] completion in + guard let self else { + return + } + self.animateOut { + completion(Void()) + } + } + + let themeUpdated = self.environment?.theme !== sheetEnvironment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let fillingSize: CGFloat + if case .regular = sheetEnvironment.metrics.widthClass { + fillingSize = min(availableSize.width, 414.0) - sheetEnvironment.safeInsets.left * 2.0 + } else { + fillingSize = min(availableSize.width, sheetEnvironment.deviceMetrics.screenSize.width) - sheetEnvironment.safeInsets.left * 2.0 + } + let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + + self.component = component + self.state = state + self.environment = sheetEnvironment + + let theme = sheetEnvironment.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 containerSize: CGSize + if !"".isEmpty, sheetEnvironment.isCentered { + let verticalInset: CGFloat = 44.0 + let maxSide = max(availableSize.width, availableSize.height) + let minSide = min(availableSize.width, availableSize.height) + containerSize = CGSize(width: min(availableSize.width - 20.0, floor(maxSide / 2.0)), height: min(availableSize.height, minSide) - verticalInset * 2.0) + if let regularMetricsSize = sheetEnvironment.regularMetricsSize { + containerSize = regularMetricsSize + } + } else { + containerSize = CGSize(width: fillingSize, height: .greatestFiniteMagnitude) + } + + let containerInset: CGFloat = sheetEnvironment.statusBarHeight + 10.0 + let clippingY: CGFloat + + self.contentView.parentState = state + let contentViewSize = self.contentView.update( + transition: transition, + component: component.content, + environment: { + environment[ChildEnvironmentType.self] + }, + containerSize: containerSize + ) + component.externalState?.contentHeight = contentViewSize.height + + if let contentView = self.contentView.view { + if contentView.superview == nil { + self.scrollContentView.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: contentViewSize)) + } + + let contentHeight = contentViewSize.height + let initialContentHeight = contentHeight + + let edgeEffectHeight: CGFloat = 80.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight)) + transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) + self.topEdgeEffectView.update(content: theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) + if self.topEdgeEffectView.superview == nil { + self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0) + } + + if let titleItem = component.titleItem { + let titleItemView: ComponentView + if let current = self.titleItemView { + titleItemView = current + } else { + titleItemView = ComponentView() + self.titleItemView = titleItemView + } + + let titleItemSize = titleItemView.update( + transition: transition, + component: titleItem, + environment: {}, + containerSize: CGSize(width: containerSize.width - 66.0 * 2.0, height: 66.0) + ) + let titleItemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - titleItemSize.width)) / 2.0, y: floorToScreenPixels(36.0 - titleItemSize.height * 0.5)), size: titleItemSize) + if let view = titleItemView.view { + if view.superview == nil { + self.navigationBarContainer.addSubview(view) + } + transition.setFrame(view: view, frame: titleItemFrame) + } + } else if let titleItemView = self.titleItemView { + self.titleItemView = nil + titleItemView.view?.removeFromSuperview() + } + + + if let leftItem = component.leftItem { + let leftItemView: ComponentView + if let current = self.leftItemView { + leftItemView = current + } else { + leftItemView = ComponentView() + self.leftItemView = leftItemView + } + + let leftItemSize = leftItemView.update( + transition: transition, + component: leftItem, + environment: {}, + containerSize: CGSize(width: 66.0, height: 66.0) + ) + let leftItemFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: leftItemSize) + if let view = leftItemView.view { + if view.superview == nil { + self.navigationBarContainer.addSubview(view) + } + transition.setFrame(view: view, frame: leftItemFrame) + } + } else if let leftItemView = self.leftItemView { + self.leftItemView = nil + leftItemView.view?.removeFromSuperview() + } + + if let rightItem = component.rightItem { + let rightItemView: ComponentView + if let current = self.rightItemView { + rightItemView = current + } else { + rightItemView = ComponentView() + self.rightItemView = rightItemView + } + + let rightItemSize = rightItemView.update( + transition: transition, + component: rightItem, + environment: {}, + containerSize: CGSize(width: 66.0, height: 66.0) + ) + let rightItemFrame = CGRect(origin: CGPoint(x: containerSize.width - 16.0 - rightItemSize.width, y: 16.0), size: rightItemSize) + if let view = rightItemView.view { + if view.superview == nil { + self.navigationBarContainer.addSubview(view) + } + transition.setFrame(view: view, frame: rightItemFrame) + } + } else if let rightItemView = self.rightItemView { + self.rightItemView = nil + rightItemView.view?.removeFromSuperview() + } + + + 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: sheetEnvironment.deviceMetrics.screenCornerRadius, bottomInset: sheetEnvironment.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 sheetEnvironment.isDisplaying && !self.didPlayAppearanceAnimation { + self.animateIn() + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 4aa7f411b7..0ca5c0ae13 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -53,6 +53,9 @@ swift_library( "//submodules/TelegramUI/Components/BatchVideoRendering", "//submodules/TelegramUI/Components/GifVideoLayer", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/LiquidLens", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift index 5028d6180f..2762c21fdd 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift @@ -419,13 +419,13 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { context.setFillColor(color.withMultipliedAlpha(0.2).cgColor) - context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 21.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 30.0).cgPath) context.fillPath() context.setFillColor(color.cgColor) - let plusSize = CGSize(width: 3.5, height: 28.0) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath) + let plusSize = CGSize(width: 4.0, height: 27.0) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height).offsetBy(dx: 0.0, dy: -18.0), cornerRadius: plusSize.width / 2.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width).offsetBy(dx: 0.0, dy: -18.0), cornerRadius: plusSize.width / 2.0).cgPath) context.fillPath() context.translateBy(x: size.width / 2.0, y: size.height / 2.0) @@ -437,7 +437,7 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { let components = string.components(separatedBy: "\n") for component in components { context.saveGState() - let attributedString = NSAttributedString(string: component, attributes: [NSAttributedString.Key.font: Font.medium(17.0), NSAttributedString.Key.foregroundColor: color]) + let attributedString = NSAttributedString(string: component, attributes: [NSAttributedString.Key.font: Font.medium(16.0), NSAttributedString.Key.foregroundColor: color]) let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 3f94209d63..2ab6b15ea0 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -4026,7 +4026,7 @@ public final class EmojiPagerContentComponent: Component { private func updateTopPanelSeparator(transition: ComponentTransition) { if let topPanelSeparator = self.topPanelSeparator { var offset = self.scrollView.contentOffset.y - let startOffset: CGFloat = 40.0 - self.topPanelHeight + let startOffset: CGFloat = 46.0 - self.topPanelHeight let endOffset: CGFloat = startOffset + 10.0 offset = min(max(offset, startOffset), endOffset) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index aa67230486..12ec0cfb7f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -403,7 +403,7 @@ public final class EntityKeyboardComponent: Component { if let _ = component.maskContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Chat/Input/Media/EntityInputSettingsIcon", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { maskContent.inputInteractionHolder.inputInteraction?.openStickerSettings?() } @@ -417,7 +417,7 @@ public final class EntityKeyboardComponent: Component { if let addImage = component.stickerContent?.inputInteractionHolder.inputInteraction?.addImage { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Media Editor/AddImage", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { addImage() } @@ -537,7 +537,7 @@ public final class EntityKeyboardComponent: Component { if let _ = component.stickerContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Chat/Input/Media/EntityInputSettingsIcon", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { stickerContent.inputInteractionHolder.inputInteraction?.openStickerSettings?() } @@ -546,7 +546,7 @@ public final class EntityKeyboardComponent: Component { if let addImage = component.stickerContent?.inputInteractionHolder.inputInteraction?.addImage { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Media Editor/AddImage", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { addImage() } @@ -649,7 +649,7 @@ public final class EntityKeyboardComponent: Component { if let _ = deleteBackwards { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Chat/Input/Media/EntityInputGlobeIcon", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { [weak self] in guard let strongSelf = self, let component = strongSelf.component else { return @@ -660,7 +660,7 @@ public final class EntityKeyboardComponent: Component { } else if let addImage = component.emojiContent?.inputInteractionHolder.inputInteraction?.addImage { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Media Editor/AddImage", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { addImage() } @@ -671,7 +671,7 @@ public final class EntityKeyboardComponent: Component { if let _ = deleteBackwards { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Chat/Input/Media/EntityInputClearIcon", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { deleteBackwards?() AudioServicesPlaySystemSound(1155) @@ -721,7 +721,8 @@ public final class EntityKeyboardComponent: Component { topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent( theme: component.theme, overflowHeight: component.hiddenInputHeight, - topInset: component.externalTopPanelContainer == nil ? 6.0 : 0.0, + topInset: component.externalTopPanelContainer == nil ? 8.0 : 0.0, + height: component.externalTopPanelContainer == nil ? 40.0 : 34.0, displayBackground: component.externalTopPanelContainer != nil ? .none : component.displayTopPanelBackground )), externalTopPanelContainer: component.externalTopPanelContainer, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelButton.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelButton.swift index faa4d87d05..c73235e76d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelButton.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelButton.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import ComponentFlow +import TelegramPresentationData import PagerComponent import ComponentDisplayAdapters import BundleIconComponent @@ -10,18 +11,18 @@ import AppBundle final class EntityKeyboardBottomPanelButton: Component { let icon: String - let color: UIColor + let theme: PresentationTheme let action: () -> Void let holdAction: (() -> Void)? init( icon: String, - color: UIColor, + theme: PresentationTheme, action: @escaping () -> Void, holdAction: (() -> Void)? = nil ) { self.icon = icon - self.color = color + self.theme = theme self.action = action self.holdAction = holdAction } @@ -30,7 +31,7 @@ final class EntityKeyboardBottomPanelButton: Component { if lhs.icon != rhs.icon { return false } - if lhs.color != rhs.color { + if lhs.theme !== rhs.theme { return false } if (lhs.holdAction == nil) != (rhs.holdAction == nil) { @@ -39,7 +40,9 @@ final class EntityKeyboardBottomPanelButton: Component { return true } - final class View: HighlightTrackingButton { + final class View: UIView { + private let backgroundView: GlassBackgroundView + let buttonView: HighlightTrackingButton let iconView: GlassBackgroundView.ContentImageView let tintMaskContainer: UIView @@ -48,15 +51,19 @@ final class EntityKeyboardBottomPanelButton: Component { var component: EntityKeyboardBottomPanelButton? - private var currentIsHighlighted: Bool = false { - didSet { - if self.currentIsHighlighted != oldValue { - self.updateAlpha(transition: .immediate) - } - } - } +// private var currentIsHighlighted: Bool = false { +// didSet { +// if self.currentIsHighlighted != oldValue { +// self.updateAlpha(transition: .immediate) +// } +// } +// } override init(frame: CGRect) { + self.backgroundView = GlassBackgroundView() + + self.buttonView = HighlightTrackingButton() + self.iconView = GlassBackgroundView.ContentImageView() self.iconView.isUserInteractionEnabled = false @@ -65,9 +72,11 @@ final class EntityKeyboardBottomPanelButton: Component { super.init(frame: frame) - self.addSubview(self.iconView) + self.addSubview(self.backgroundView) + self.backgroundView.contentView.addSubview(self.iconView) + self.backgroundView.contentView.addSubview(self.buttonView) - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.buttonView.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -86,65 +95,65 @@ final class EntityKeyboardBottomPanelButton: Component { } } - override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - self.currentIsHighlighted = true - - self.holdActionTriggerred = false - - if self.component?.holdAction != nil { - self.holdActionTriggerred = true - self.component?.action() - - self.holdActionTimer?.invalidate() - let holdActionTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.holdActionTimer?.invalidate() - strongSelf.component?.holdAction?() - strongSelf.beginExecuteHoldActionTimer() - }) - self.holdActionTimer = holdActionTimer - RunLoop.main.add(holdActionTimer, forMode: .common) - } - - return super.beginTracking(touch, with: event) - } +// override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { +// self.currentIsHighlighted = true +// +// self.holdActionTriggerred = false +// +// if self.component?.holdAction != nil { +// self.holdActionTriggerred = true +// self.component?.action() +// +// self.holdActionTimer?.invalidate() +// let holdActionTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in +// guard let strongSelf = self else { +// return +// } +// strongSelf.holdActionTimer?.invalidate() +// strongSelf.component?.holdAction?() +// strongSelf.beginExecuteHoldActionTimer() +// }) +// self.holdActionTimer = holdActionTimer +// RunLoop.main.add(holdActionTimer, forMode: .common) +// } +// +// return super.beginTracking(touch, with: event) +// } - private func beginExecuteHoldActionTimer() { - self.holdActionTimer?.invalidate() - let holdActionTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.component?.holdAction?() - }) - self.holdActionTimer = holdActionTimer - RunLoop.main.add(holdActionTimer, forMode: .common) - } +// private func beginExecuteHoldActionTimer() { +// self.holdActionTimer?.invalidate() +// let holdActionTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] _ in +// guard let strongSelf = self else { +// return +// } +// strongSelf.component?.holdAction?() +// }) +// self.holdActionTimer = holdActionTimer +// RunLoop.main.add(holdActionTimer, forMode: .common) +// } - override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { - self.currentIsHighlighted = false - - self.holdActionTimer?.invalidate() - self.holdActionTimer = nil - - super.endTracking(touch, with: event) - } - - override public func cancelTracking(with event: UIEvent?) { - self.currentIsHighlighted = false - - self.holdActionTimer?.invalidate() - self.holdActionTimer = nil - - super.cancelTracking(with: event) - } +// override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { +// self.currentIsHighlighted = false +// +// self.holdActionTimer?.invalidate() +// self.holdActionTimer = nil +// +// super.endTracking(touch, with: event) +// } +// +// override public func cancelTracking(with event: UIEvent?) { +// self.currentIsHighlighted = false +// +// self.holdActionTimer?.invalidate() +// self.holdActionTimer = nil +// +// super.cancelTracking(with: event) +// } - private func updateAlpha(transition: ComponentTransition) { - let alpha: CGFloat = self.currentIsHighlighted ? 0.6 : 1.0 - transition.setAlpha(view: self.iconView, alpha: alpha) - } +// private func updateAlpha(transition: ComponentTransition) { +// let alpha: CGFloat = self.currentIsHighlighted ? 0.6 : 1.0 +// transition.setAlpha(view: self.iconView, alpha: alpha) +// } func update(component: EntityKeyboardBottomPanelButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.icon != component.icon { @@ -153,15 +162,21 @@ final class EntityKeyboardBottomPanelButton: Component { self.component = component - self.iconView.tintColor = component.color + self.iconView.tintColor = component.theme.chat.inputPanel.panelControlColor - let size = CGSize(width: 38.0, height: 38.0) + let size = CGSize(width: 40.0, height: 40.0) if let image = self.iconView.image { let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size) self.iconView.frame = iconFrame } + let tintColor: GlassBackgroundView.TintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: true, transition: transition) + + self.buttonView.frame = CGRect(origin: .zero, size: size) + return size } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index 5db313c31f..1544d1d957 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -8,6 +8,9 @@ import TelegramCore import ComponentDisplayAdapters import BundleIconComponent import GlassBackgroundComponent +import EdgeEffect +import LiquidLens +import TabSelectionRecognizer private final class BottomPanelIconComponent: Component { let title: String @@ -57,22 +60,15 @@ private final class BottomPanelIconComponent: Component { super.init(frame: frame) self.addSubview(self.contentView) - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.component?.action() - } - } - func update(component: BottomPanelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.title != component.title { - let text = NSAttributedString(string: component.title, font: Font.medium(15.0), textColor: .white) + let text = NSAttributedString(string: component.title, font: Font.medium(14.0), textColor: .white) let textBounds = text.boundingRect(with: CGSize(width: 120.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) self.contentView.image = generateImage(CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height)), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -89,19 +85,9 @@ private final class BottomPanelIconComponent: Component { let textSize = self.contentView.image?.size ?? CGSize() let size = CGSize(width: textSize.width + textInset * 2.0, height: 28.0) - let color = component.theme.chat.inputPanel.inputControlColor + self.contentView.tintColor = component.theme.chat.inputPanel.panelControlColor - if self.contentView.tintColor != color { - if !transition.animation.isImmediate { - UIView.animate(withDuration: 0.15, delay: 0.0, options: [], animations: { - self.contentView.tintColor = color - }, completion: nil) - } else { - self.contentView.tintColor = color - } - } - - transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: (size.height - textSize.height) / 2.0 - 1.0), size: textSize)) + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: (size.height - textSize.height) / 2.0), size: textSize)) return size } @@ -163,17 +149,27 @@ final class EntityKeyboardBottomPanelComponent: Component { private var leftAccessoryButton: AccessoryButtonView? private var rightAccessoryButton: AccessoryButtonView? - private var iconViews: [AnyHashable: ComponentHostView] = [:] - private var highlightedIconBackgroundView: UIView - private var highlightedTintIconBackgroundView: UIView + private let edgeEffectView: EdgeEffectView + private let backgroundContainer: GlassBackgroundContainerView + private let liquidLensView: LiquidLensView + private var itemViews: [AnyHashable: ComponentHostView] = [:] + private var selectedItemViews: [AnyHashable: ComponentHostView] = [:] + + private var tabSelectionRecognizer: TabSelectionRecognizer? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? + let tintContentMask: UIView private var component: EntityKeyboardBottomPanelComponent? + private var state: EmptyComponentState? + private var environment: PagerComponentPanelEnvironment? override init(frame: CGRect) { self.tintContentMask = UIView() + self.edgeEffectView = EdgeEffectView() + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true, customBlurRadius: 10.0) self.separatorView = UIView() @@ -182,51 +178,102 @@ final class EntityKeyboardBottomPanelComponent: Component { self.tintSeparatorView.isUserInteractionEnabled = false self.tintSeparatorView.backgroundColor = UIColor(white: 0.0, alpha: 0.7) - self.tintContentMask.addSubview(self.tintSeparatorView) - - self.highlightedIconBackgroundView = UIView() - self.highlightedIconBackgroundView.isUserInteractionEnabled = false - self.highlightedIconBackgroundView.layer.cornerRadius = 10.0 - self.highlightedIconBackgroundView.clipsToBounds = true - - self.highlightedTintIconBackgroundView = UIView() - self.highlightedTintIconBackgroundView.isUserInteractionEnabled = false - self.highlightedTintIconBackgroundView.layer.cornerRadius = 10.0 - self.highlightedTintIconBackgroundView.clipsToBounds = true - self.highlightedTintIconBackgroundView.backgroundColor = UIColor(white: 0.0, alpha: 0.1) - - self.tintContentMask.addSubview(self.highlightedTintIconBackgroundView) - + self.backgroundContainer = GlassBackgroundContainerView() + self.liquidLensView = LiquidLensView(useBackgroundContainer: false) + super.init(frame: frame) - self.addSubview(self.backgroundView) - self.addSubview(self.highlightedIconBackgroundView) - self.addSubview(self.separatorView) + self.addSubview(self.edgeEffectView) + + self.addSubview(self.backgroundContainer) + self.backgroundContainer.contentView.addSubview(self.liquidLensView) + + let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + self.tabSelectionRecognizer = tabSelectionRecognizer + self.liquidLensView.addGestureRecognizer(tabSelectionRecognizer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + private func item(at point: CGPoint) -> AnyHashable? { + var closestItem: (AnyHashable, CGFloat)? + for (id, itemView) in self.itemViews { + if itemView.frame.contains(point) { + return id + } else { + let distance = abs(point.x - itemView.center.x) + if let closestItemValue = closestItem { + if closestItemValue.1 > distance { + closestItem = (id, distance) + } + } else { + closestItem = (id, distance) + } + } + } + return closestItem?.0 + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let environment = self.environment else { + return + } + let location = recognizer.location(in: self.liquidLensView.contentView) + switch recognizer.state { + case .began: + if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { + let startX = itemView.frame.minX - 4.0 + self.selectionGestureState = (startX, startX, itemId) + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + case .changed: + if var selectionGestureState = self.selectionGestureState { + selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x + if let itemId = self.item(at: location) { + selectionGestureState.itemId = itemId + } + self.selectionGestureState = selectionGestureState + self.state?.updated(transition: .immediate, isLocal: true) + } + case .ended, .cancelled: + if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state { + guard let item = environment.contentIcons.first(where: { $0.id == selectionGestureState.itemId }) else { + return + } + environment.navigateToContentId(item.id) + } + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + default: + break + } + } + func update(component: EntityKeyboardBottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.theme !== component.theme { self.separatorView.backgroundColor = component.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) self.backgroundView.updateColor(color: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), transition: .immediate) - self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor } let intrinsicHeight: CGFloat = 34.0 - let height = intrinsicHeight + component.containerInsets.bottom + let height = intrinsicHeight + component.containerInsets.bottom + 20.0 let accessoryButtonOffset: CGFloat if component.containerInsets.bottom > 0.0 { - accessoryButtonOffset = 2.0 + accessoryButtonOffset = 0.0 } else { accessoryButtonOffset = -2.0 } + self.component = component + self.state = state let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value + self.environment = panelEnvironment let activeContentId = panelEnvironment.activeContentId var leftAccessoryButtonComponent: AnyComponentWithIdentity? @@ -257,7 +304,7 @@ final class EntityKeyboardBottomPanelComponent: Component { environment: {}, containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight) ) - let leftAccessoryButtonFrame = CGRect(origin: CGPoint(x: component.containerInsets.left + 2.0, y: accessoryButtonOffset), size: leftAccessoryButtonSize) + let leftAccessoryButtonFrame = CGRect(origin: CGPoint(x: component.containerInsets.left + 18.0, y: accessoryButtonOffset), size: leftAccessoryButtonSize) leftAccessoryButtonTransition.setFrame(view: leftAccessoryButton.view, frame: leftAccessoryButtonFrame) if let leftAccessoryButtonView = leftAccessoryButton.view.componentView as? PagerTopPanelView { if leftAccessoryButtonView.tintContentMask.superview == nil { @@ -328,7 +375,7 @@ final class EntityKeyboardBottomPanelComponent: Component { containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight) ) - let rightAccessoryButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - component.containerInsets.right - 2.0 - rightAccessoryButtonSize.width, y: accessoryButtonOffset), size: rightAccessoryButtonSize) + let rightAccessoryButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - component.containerInsets.right - 18.0 - rightAccessoryButtonSize.width, y: accessoryButtonOffset), size: rightAccessoryButtonSize) rightAccessoryButtonTransition.setFrame(view: rightAccessoryButton.view, frame: rightAccessoryButtonFrame) if let rightAccessoryButtonView = rightAccessoryButton.view.componentView as? PagerTopPanelView { if rightAccessoryButtonView.tintContentMask.superview == nil { @@ -374,23 +421,32 @@ final class EntityKeyboardBottomPanelComponent: Component { var iconInfos: [AnyHashable: (size: CGSize, transition: ComponentTransition)] = [:] var iconTotalSize = CGSize() - let iconSpacing: CGFloat = 4.0 + let iconSpacing: CGFloat = 0.0 let navigateToContentId = panelEnvironment.navigateToContentId + var lensSelection: (x: CGFloat, width: CGFloat) = (0.0, 0.0) + if panelEnvironment.contentIcons.count > 1 { for icon in panelEnvironment.contentIcons { validIconIds.append(icon.id) var iconTransition = transition let iconView: ComponentHostView - if let current = self.iconViews[icon.id] { + let selectedIconView: ComponentHostView + if let current = self.itemViews[icon.id], let currentSelected = self.selectedItemViews[icon.id] { iconView = current + selectedIconView = currentSelected } else { iconTransition = .immediate iconView = ComponentHostView() - self.iconViews[icon.id] = iconView - self.addSubview(iconView) + iconView.isUserInteractionEnabled = false + selectedIconView = ComponentHostView() + selectedIconView.isUserInteractionEnabled = false + self.itemViews[icon.id] = iconView + self.selectedItemViews[icon.id] = selectedIconView + self.liquidLensView.contentView.addSubview(iconView) + self.liquidLensView.selectedContentView.addSubview(selectedIconView) } let iconSize = iconView.update( @@ -407,65 +463,70 @@ final class EntityKeyboardBottomPanelComponent: Component { containerSize: CGSize(width: 28.0, height: 28.0) ) + let _ = selectedIconView.update( + transition: iconTransition, + component: AnyComponent(BottomPanelIconComponent( + title: icon.title, + isHighlighted: icon.id == activeContentId, + theme: component.theme, + action: { + navigateToContentId(icon.id) + } + )), + environment: {}, + containerSize: CGSize(width: 28.0, height: 28.0) + ) + iconInfos[icon.id] = (size: iconSize, transition: iconTransition) if !iconTotalSize.width.isZero { - iconTotalSize.width += iconSpacing + iconTotalSize.width += iconSpacing - 8.0 } iconTotalSize.width += iconSize.width iconTotalSize.height = max(iconTotalSize.height, iconSize.height) } } - - var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0)) - if component.containerInsets.bottom > 0.0 { - nextIconOrigin.y += 3.0 - } + + let tabsSize = CGSize(width: iconTotalSize.width, height: 40.0) + var nextIconOrigin = CGPoint(x: floor((tabsSize.width - iconTotalSize.width) / 2.0), y: floor((tabsSize.height - iconTotalSize.height) / 2.0)) + + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: .zero, size: availableSize)) + self.backgroundContainer.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) if panelEnvironment.contentIcons.count > 1 { for icon in panelEnvironment.contentIcons { - guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else { + guard let iconInfo = iconInfos[icon.id], let iconView = self.itemViews[icon.id], let selectedIconView = self.selectedItemViews[icon.id] else { continue } let iconFrame = CGRect(origin: nextIconOrigin, size: iconInfo.size) iconInfo.transition.setFrame(view: iconView, frame: iconFrame, completion: nil) - - if let iconView = iconView.componentView as? BottomPanelIconComponent.View { - if iconView.tintMaskContainer.superview == nil { - self.tintContentMask.addSubview(iconView.tintMaskContainer) - } - iconInfo.transition.setFrame(view: iconView.tintMaskContainer, frame: iconFrame, completion: nil) - } + iconInfo.transition.setFrame(view: selectedIconView, frame: iconFrame, completion: nil) if let activeContentId = activeContentId, activeContentId == icon.id { - self.highlightedIconBackgroundView.isHidden = false - self.highlightedTintIconBackgroundView.isHidden = false - transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame) - transition.setFrame(view: self.highlightedTintIconBackgroundView, frame: iconFrame) - - let cornerRadius: CGFloat = min(iconFrame.width, iconFrame.height) / 2.0 - transition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: cornerRadius) - transition.setCornerRadius(layer: self.highlightedTintIconBackgroundView.layer, cornerRadius: cornerRadius) + lensSelection = (iconFrame.origin.x, iconFrame.width) } - - nextIconOrigin.x += iconInfo.size.width + iconSpacing + nextIconOrigin.x += iconInfo.size.width + iconSpacing - 8.0 } } - - if activeContentId == nil { - self.highlightedIconBackgroundView.isHidden = true + + if let selectionGestureState = self.selectionGestureState { + lensSelection = (selectionGestureState.currentX, lensSelection.width) } + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabsSize.width) / 2.0), y: 0.0), size: tabsSize)) + self.liquidLensView.update(size: tabsSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: tabsSize.height), isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: activeContentId == nil, transition: transition) + var removedIconViewIds: [AnyHashable] = [] - for (id, iconView) in self.iconViews { + for (id, iconView) in self.itemViews { if !validIconIds.contains(id) { removedIconViewIds.append(id) iconView.removeFromSuperview() } } + for id in removedIconViewIds { - self.iconViews.removeValue(forKey: id) + self.itemViews.removeValue(forKey: id) } transition.setFrame(view: self.separatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) @@ -473,8 +534,11 @@ final class EntityKeyboardBottomPanelComponent: Component { transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height))) //self.backgroundView.update(size: CGSize(width: availableSize.width, height: height), transition: transition.containedViewLayoutTransition) - - self.component = component + + let edgeEffectHeight: CGFloat = 80.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: height - edgeEffectHeight), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) + transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) + self.edgeEffectView.update(content: component.theme.chat.inputMediaPanel.backgroundColor.withMultipliedAlpha(0.8), rect: edgeEffectFrame, edge: .bottom, edgeSize: min(edgeEffectHeight, 50.0), transition: transition) return CGSize(width: availableSize.width, height: height) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index a7d0dfc927..83c09abbe0 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -9,15 +9,18 @@ import Postbox public final class EntityKeyboardTopContainerPanelEnvironment: Equatable { let isContentInFocus: Bool + let height: CGFloat let visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)> let isExpandedUpdated: (Bool, ComponentTransition) -> Void init( isContentInFocus: Bool, + height: CGFloat, visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)>, isExpandedUpdated: @escaping (Bool, ComponentTransition) -> Void ) { self.isContentInFocus = isContentInFocus + self.height = height self.visibilityFractionUpdated = visibilityFractionUpdated self.isExpandedUpdated = isExpandedUpdated } @@ -26,6 +29,9 @@ public final class EntityKeyboardTopContainerPanelEnvironment: Equatable { if lhs.isContentInFocus != rhs.isContentInFocus { return false } + if lhs.height != rhs.height { + return false + } if lhs.visibilityFractionUpdated !== rhs.visibilityFractionUpdated { return false } @@ -39,17 +45,20 @@ final class EntityKeyboardTopContainerPanelComponent: Component { let theme: PresentationTheme let overflowHeight: CGFloat let topInset: CGFloat + let height: CGFloat let displayBackground: EntityKeyboardComponent.DisplayTopPanelBackground init( theme: PresentationTheme, overflowHeight: CGFloat, topInset: CGFloat, + height: CGFloat, displayBackground: EntityKeyboardComponent.DisplayTopPanelBackground ) { self.theme = theme self.overflowHeight = overflowHeight self.topInset = topInset + self.height = height self.displayBackground = displayBackground } @@ -60,6 +69,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component { if lhs.overflowHeight != rhs.overflowHeight { return false } + if lhs.height != rhs.height { + return false + } if lhs.topInset != rhs.topInset { return false } @@ -105,7 +117,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { } func update(component: EntityKeyboardTopContainerPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let intrinsicHeight: CGFloat = 34.0 + let intrinsicHeight: CGFloat = component.height let height = intrinsicHeight + component.topInset let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value @@ -165,6 +177,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { environment: { EntityKeyboardTopContainerPanelEnvironment( isContentInFocus: panelEnvironment.isContentInFocus, + height: intrinsicHeight, visibilityFractionUpdated: panelView.visibilityFractionUpdated, isExpandedUpdated: { [weak self] isExpanded, transition in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index d6c9379c2e..a28088ad5d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -2009,7 +2009,7 @@ public final class EntityKeyboardTopPanelComponent: Component { let panelEnvironment = environment[EntityKeyboardTopContainerPanelEnvironment.self].value self.environment = panelEnvironment - let isExpanded = availableSize.height > 34.0 + let isExpanded = availableSize.height > panelEnvironment.height let wasExpanded = self.isExpanded self.isExpanded = isExpanded diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index bad7a1666f..c570354d66 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -23,7 +23,6 @@ public final class GiftCompositionComponent: Component { public fileprivate(set) var previewSymbol: StarGift.UniqueGift.Attribute? public init() { - self.previewPatternColor = nil } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift index fde4bd9984..48ad09ede5 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift @@ -3,238 +3,237 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox +import ComponentFlow import TelegramCore import TelegramPresentationData -import TelegramUIPreferences import AccountContext import AppBundle -import AvatarNode -import Markdown +import AlertComponent -private final class GiftAuctionTransferAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let title: String - private let text: String - - private let titleNode: ASTextNode - private let textNode: ASTextNode - private let avatarNode: AvatarNode - private let arrowNode: ASImageNode - private let secondAvatarNode: AvatarNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeer: EnginePeer, toPeer: EnginePeer, title: String, text: String, actions: [TextAlertAction]) { - self.strings = strings - self.title = title - self.text = text - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 0 - - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 0 - - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) - - self.arrowNode = ASImageNode() - self.arrowNode.displaysAsynchronously = false - self.arrowNode.displayWithoutProcessing = true - - self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isLayerBacked = true - - self.actionNodes = actions.map { action -> TextAlertContentActionNode in - return TextAlertContentActionNode(theme: theme, action: action) - } - - var actionVerticalSeparators: [ASDisplayNode] = [] - if actions.count > 1 { - for _ in 0 ..< actions.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) - self.addSubnode(self.avatarNode) - self.addSubnode(self.arrowNode) - self.addSubnode(self.secondAvatarNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - - self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeer) - self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer) - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), - bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), - link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), - linkAttribute: { url in - return ("URL", url) - } - ), textAlignment: .center) - self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor) - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - - let avatarSize = CGSize(width: 60.0, height: 60.0) - self.avatarNode.updateSize(size: avatarSize) - - let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) - transition.updateFrame(node: self.avatarNode, frame: avatarFrame) - - if let arrowImage = self.arrowNode.image { - let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) - transition.updateFrame(node: self.arrowNode, frame: arrowFrame) - } - - let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) - transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame) - - origin.y += avatarSize.height + 10.0 +//private final class GiftAuctionTransferAlertContentNode: AlertContentNode { +// private let strings: PresentationStrings +// private let title: String +// private let text: String +// +// private let titleNode: ASTextNode +// private let textNode: ASTextNode +// private let avatarNode: AvatarNode +// private let arrowNode: ASImageNode +// private let secondAvatarNode: AvatarNode +// +// private let actionNodesSeparator: ASDisplayNode +// private let actionNodes: [TextAlertContentActionNode] +// private let actionVerticalSeparators: [ASDisplayNode] +// +// private var validLayout: CGSize? +// +// override var dismissOnOutsideTap: Bool { +// return self.isUserInteractionEnabled +// } +// +// init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeer: EnginePeer, toPeer: EnginePeer, title: String, text: String, actions: [TextAlertAction]) { +// self.strings = strings +// self.title = title +// self.text = text +// +// self.titleNode = ASTextNode() +// self.titleNode.maximumNumberOfLines = 0 +// +// self.textNode = ASTextNode() +// self.textNode.maximumNumberOfLines = 0 +// +// self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) +// +// self.arrowNode = ASImageNode() +// self.arrowNode.displaysAsynchronously = false +// self.arrowNode.displayWithoutProcessing = true +// +// self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) +// +// self.actionNodesSeparator = ASDisplayNode() +// self.actionNodesSeparator.isLayerBacked = true +// +// self.actionNodes = actions.map { action -> TextAlertContentActionNode in +// return TextAlertContentActionNode(theme: theme, action: action) +// } +// +// var actionVerticalSeparators: [ASDisplayNode] = [] +// if actions.count > 1 { +// for _ in 0 ..< actions.count - 1 { +// let separatorNode = ASDisplayNode() +// separatorNode.isLayerBacked = true +// actionVerticalSeparators.append(separatorNode) +// } +// } +// self.actionVerticalSeparators = actionVerticalSeparators +// +// super.init() +// +// self.addSubnode(self.titleNode) +// self.addSubnode(self.textNode) +// self.addSubnode(self.avatarNode) +// self.addSubnode(self.arrowNode) +// self.addSubnode(self.secondAvatarNode) +// +// self.addSubnode(self.actionNodesSeparator) +// +// for actionNode in self.actionNodes { +// self.addSubnode(actionNode) +// } +// +// for separatorNode in self.actionVerticalSeparators { +// self.addSubnode(separatorNode) +// } +// +// self.updateTheme(theme) +// +// self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeer) +// self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer) +// } +// +// override func updateTheme(_ theme: AlertControllerTheme) { +// self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) +// self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes( +// body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), +// bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), +// link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), +// linkAttribute: { url in +// return ("URL", url) +// } +// ), textAlignment: .center) +// self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor) +// +// self.actionNodesSeparator.backgroundColor = theme.separatorColor +// for actionNode in self.actionNodes { +// actionNode.updateTheme(theme) +// } +// for separatorNode in self.actionVerticalSeparators { +// separatorNode.backgroundColor = theme.separatorColor +// } +// +// if let size = self.validLayout { +// _ = self.updateLayout(size: size, transition: .immediate) +// } +// } +// +// override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { +// var size = size +// size.width = min(size.width, 270.0) +// +// self.validLayout = size +// +// var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) +// +// let avatarSize = CGSize(width: 60.0, height: 60.0) +// self.avatarNode.updateSize(size: avatarSize) +// +// let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) +// transition.updateFrame(node: self.avatarNode, frame: avatarFrame) +// +// if let arrowImage = self.arrowNode.image { +// let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) +// transition.updateFrame(node: self.arrowNode, frame: arrowFrame) +// } +// +// let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) +// transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame) +// +// origin.y += avatarSize.height + 10.0 +// +// let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height)) +// transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) +// origin.y += titleSize.height + 4.0 +// +// let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) +// transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) +// origin.y += textSize.height + 10.0 +// +// let actionButtonHeight: CGFloat = 44.0 +// var minActionsWidth: CGFloat = 0.0 +// let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) +// let actionTitleInsets: CGFloat = 8.0 +// +// var effectiveActionLayout = TextAlertContentActionLayout.horizontal +// for actionNode in self.actionNodes { +// let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) +// if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { +// effectiveActionLayout = .vertical +// } +// switch effectiveActionLayout { +// case .horizontal: +// minActionsWidth += actionTitleSize.width + actionTitleInsets +// case .vertical: +// minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) +// } +// } +// +// let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) +// +// let contentWidth = max(size.width, minActionsWidth) +// +// var actionsHeight: CGFloat = 0.0 +// switch effectiveActionLayout { +// case .horizontal: +// actionsHeight = actionButtonHeight +// case .vertical: +// actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) +// } +// +// let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) +// transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) +// +// var actionOffset: CGFloat = 0.0 +// let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) +// var separatorIndex = -1 +// var nodeIndex = 0 +// for actionNode in self.actionNodes { +// if separatorIndex >= 0 { +// let separatorNode = self.actionVerticalSeparators[separatorIndex] +// switch effectiveActionLayout { +// case .horizontal: +// transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) +// case .vertical: +// transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) +// } +// } +// separatorIndex += 1 +// +// let currentActionWidth: CGFloat +// switch effectiveActionLayout { +// case .horizontal: +// if nodeIndex == self.actionNodes.count - 1 { +// currentActionWidth = resultSize.width - actionOffset +// } else { +// currentActionWidth = actionWidth +// } +// case .vertical: +// currentActionWidth = resultSize.width +// } +// +// let actionNodeFrame: CGRect +// switch effectiveActionLayout { +// case .horizontal: +// actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) +// actionOffset += currentActionWidth +// case .vertical: +// actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) +// actionOffset += actionButtonHeight +// } +// +// transition.updateFrame(node: actionNode, frame: actionNodeFrame) +// +// nodeIndex += 1 +// } +// +// return resultSize +// } +//} - let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) - origin.y += titleSize.height + 4.0 - - let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) - origin.y += textSize.height + 10.0 - - let actionButtonHeight: CGFloat = 44.0 - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = TextAlertContentActionLayout.horizontal - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { - effectiveActionLayout = .vertical - } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } - } - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) - - let contentWidth = max(size.width, minActionsWidth) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) - transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - switch effectiveActionLayout { - case .horizontal: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - case .vertical: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - } - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical: - currentActionWidth = resultSize.width - } - - let actionNodeFrame: CGRect - switch effectiveActionLayout { - case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } - - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - return resultSize - } -} - -func giftAuctionTransferController(context: AccountContext, fromPeer: EnginePeer, toPeer: EnginePeer, commit: @escaping () -> Void) -> AlertController { +func giftAuctionTransferController(context: AccountContext, fromPeer: EnginePeer, toPeer: EnginePeer, commit: @escaping () -> Void) -> AlertScreen { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings + let title = strings.Gift_AuctionTransfer_Title let text: String if fromPeer.id == context.account.peerId { text = strings.Gift_AuctionTransfer_TextFromYourself(toPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string @@ -243,25 +242,74 @@ func giftAuctionTransferController(context: AccountContext, fromPeer: EnginePeer } else { text = strings.Gift_AuctionTransfer_Text(fromPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), toPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string } + + var content: [AnyComponentWithIdentity] = [] +// content.append(AnyComponentWithIdentity( +// id: "header", +// component: AnyComponent( +// AlertTransferHeaderComponent( +// fromComponent: AnyComponentWithIdentity(id: "gift", component: AnyComponent( +// AvatarComponent( +// context: context, +// theme: presentationData.theme, +// peer: fromPeer +// ) +// )), +// toComponent: AnyComponentWithIdentity(id: "avatar", component: AnyComponent( +// AvatarComponent( +// context: context, +// theme: presentationData.theme, +// peer: toPeer +// ) +// )) +// ) +// ) +// )) + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + + let alertController = AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical, dismissOnOutsideTap: true, allowInputInset: false), + content: content, + actions: [ + .init(title: strings.Gift_AuctionTransfer_Change, type: .default, action: { + commit() + }), + .init(title: strings.Common_Cancel) + ] + ) + return alertController + - var dismissImpl: ((Bool) -> Void)? - var contentNode: GiftAuctionTransferAlertContentNode? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: strings.Gift_AuctionTransfer_Change, action: { - dismissImpl?(true) - commit() - })] - - contentNode = GiftAuctionTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, fromPeer: fromPeer, toPeer: toPeer, title: strings.Gift_AuctionTransfer_Title, text: text, actions: actions) - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller +// var dismissImpl: ((Bool) -> Void)? +// var contentNode: GiftAuctionTransferAlertContentNode? +// let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { +// dismissImpl?(true) +// }), TextAlertAction(type: .defaultAction, title: strings.Gift_AuctionTransfer_Change, action: { +// dismissImpl?(true) +// commit() +// })] +// +// contentNode = GiftAuctionTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, fromPeer: fromPeer, toPeer: toPeer, title: strings.Gift_AuctionTransfer_Title, text: text, actions: actions) +// +// let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) +// dismissImpl = { [weak controller] animated in +// if animated { +// controller?.dismissAnimated() +// } else { +// controller?.dismiss() +// } +// } +// return controller } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 14026bf06d..4cee9f10fa 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -991,7 +991,7 @@ final class GiftOptionsScreenComponent: Component { controller.present(alertController, in: .current) dismissAlertImpl = { [weak alertController] in - alertController?.dismissAnimated() + alertController?.dismiss() } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index a5d6d5dcee..cf236d2fe6 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -64,6 +64,9 @@ swift_library( "//submodules/TelegramUI/Components/SegmentControlComponent", "//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent", "//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent", + "//submodules/TelegramUI/Components/Gifts/TableComponent", + "//submodules/TelegramUI/Components/Gifts/PeerTableCellComponent", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift index 1d4efafe49..bbf99fe9a0 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift @@ -19,6 +19,8 @@ import TelegramStringFormatting import GlassBarButtonComponent import GiftItemComponent import EdgeEffect +import TableComponent +import PeerTableCellComponent private final class GiftAuctionAcquiredScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -373,7 +375,7 @@ private final class GiftAuctionAcquiredScreenComponent: Component { title: environment.strings.Gift_Acquired_Recipient, component: AnyComponent(Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: environment.theme, strings: environment.strings, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift index 3b872bdf88..c7004ea02e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift @@ -372,7 +372,7 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { if self.backgroundHandleView.image == nil { self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) } - self.backgroundHandleView.tintColor = environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(environment.theme.overallDarkAppearance ? 0.2 : 0.07) + self.backgroundHandleView.tintColor = theme.list.itemPrimaryTextColor.withMultipliedAlpha(theme.overallDarkAppearance ? 0.2 : 0.07) 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) @@ -383,13 +383,13 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { transition: .immediate, component: AnyComponent(GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, isDark: environment.theme.overallDarkAppearance, state: .generic, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", - tintColor: environment.theme.chat.inputPanel.panelControlColor + tintColor: theme.chat.inputPanel.panelControlColor ) )), action: { [weak self] _ in @@ -413,14 +413,13 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { let containerInset: CGFloat = environment.statusBarHeight + 10.0 contentHeight += environment.safeInsets.bottom - var initialContentHeight = contentHeight let clippingY: CGFloat let titleText: String = environment.strings.Gift_ActiveAuctions_Title(Int32(self.auctionStates.count)) let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -434,12 +433,12 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { transition.setFrame(view: titleView, frame: titleFrame) } - initialContentHeight = contentHeight + let initialContentHeight = contentHeight let edgeEffectHeight: CGFloat = 80.0 let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight)) transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) - self.topEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) + self.topEdgeEffectView.update(content: theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) if self.topEdgeEffectView.superview == nil { self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 04a2bb12ba..e678255b63 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -28,6 +28,7 @@ import ButtonComponent import UndoUI import LottieComponent import AnimatedTextComponent +import TableComponent private final class GiftAuctionViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -1075,7 +1076,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { guard let state, let attributes = state.giftUpgradeAttributes else { return } - let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsPreviewScreen(context: component.context, gift: .generic(gift), attributes: attributes) + let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(context: component.context, gift: .generic(gift), attributes: attributes, selectedAttributes: nil, focusedAttribute: nil) environment.controller()?.push(variantsController) }, animateScale: false), availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift index 69d21542ad..69cdbadcd2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift @@ -3,478 +3,29 @@ import UIKit import AsyncDisplayKit import Display import ComponentFlow +import SwiftSignalKit 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 BalancedTextComponent +import BundleIconComponent 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 amount: CurrencyAmount - 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 valueDelta = 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, - amount: CurrencyAmount, - actions: [TextAlertAction] - ) { - self.context = context - self.strings = strings - self.presentationTheme = ptheme - self.title = title - self.text = text - self.amount = amount - 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 - )) - } - } - - 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 - } - - var valueDeltaHeight: CGFloat = 0.0 - if let valueAmount = self.gift.valueUsdAmount { - let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) - - let usdRate: Double - switch self.amount.currency { - case .stars: - usdRate = Double(resaleConfiguration.usdWithdrawRate) / 1000.0 / 100.0 - case .ton: - usdRate = Double(resaleConfiguration.tonUsdRate) / 1000.0 / 1000000.0 - } - let offerUsdValue = Double(self.amount.amount.value) * usdRate - let giftUsdValue = Double(valueAmount) / 100.0 - - let fraction = giftUsdValue / offerUsdValue - let percentage = Int(fraction * 100) - 100 - - if percentage > 20 { - let textColor = self.presentationTheme.list.itemDestructiveColor - let markdownAttributes = 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) - } - ) - let valueDeltaSize = self.valueDelta.update( - transition: .immediate, - component: AnyComponent( - BalancedTextComponent( - text: .markdown(text: strings.Chat_GiftPurchaseOffer_AcceptConfirmation_BadValue("\(percentage)%").string, attributes: markdownAttributes), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - ) - ), - environment: {}, - containerSize: CGSize(width: contentWidth - 32.0, height: size.height) - ) - let valueDeltaFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - valueDeltaSize.width) / 2.0), y: avatarSize.height + titleSize.height + textSize.height + 73.0 + tableSize.height), size: valueDeltaSize) - if let view = self.valueDelta.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.frame = valueDeltaFrame - } - valueDeltaHeight += valueDeltaSize.height + 10.0 - } - } - - let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + tableSize.height + actionsHeight + valueDeltaHeight + 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 - } -} +import AlertComponent +import TableComponent public func giftOfferAlertController( context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)?, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void -) -> AlertController { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } +) -> ViewController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings let title = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Title @@ -502,28 +53,367 @@ public func giftOfferAlertController( let giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: presentationData.dateTimeFormat))" let text = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text(giftTitle, peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), priceString, finalPriceString).string - var contentNode: GiftOfferAlertContentNode? - var dismissImpl: ((Bool) -> Void)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in - contentNode?.inProgress = true - commit() - dismissImpl?(true) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - })] + let tableFont = Font.regular(15.0) + let tableTextColor = presentationData.theme.list.itemPrimaryTextColor - contentNode = GiftOfferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, amount: amount, actions: actions) + let modelButtonTag = GenericComponentViewTag() + let backdropButtonTag = GenericComponentViewTag() + let symbolButtonTag = GenericComponentViewTag() + var showAttributeInfoImpl: ((Any, String) -> Void)? - let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: nil, chatPeerId: context.account.peerId, showBalance: false) - contentNode?.getController = { [weak controller] in - return controller + 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 gift.attributes { + attributeMap[attribute.attributeType] = attribute } - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() + + 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 = 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 = 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 = 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: context, + text: formatPercentage(percentage), + color: presentationData.theme.list.itemAccentColor + )), + action: { + showAttributeInfoImpl?(tag, 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 + )) } } - return controller + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertTransferHeaderComponent( + fromComponent: AnyComponentWithIdentity(id: "gift", component: AnyComponent( + GiftItemComponent( + context: context, + theme: presentationData.theme, + strings: strings, + peer: nil, + subject: .uniqueGift(gift: gift, price: nil), + mode: .thumbnail + ) + )), + toComponent: AnyComponentWithIdentity(id: "avatar", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: peer + ) + )) + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + content.append(AnyComponentWithIdentity( + id: "table", + component: AnyComponent( + AlertTableComponent(items: tableItems) + ) + )) + + if let valueAmount = gift.valueUsdAmount { + let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + + let usdRate: Double + switch amount.currency { + case .stars: + usdRate = Double(resaleConfiguration.usdWithdrawRate) / 1000.0 / 100.0 + case .ton: + usdRate = Double(resaleConfiguration.tonUsdRate) / 1000.0 / 1000000.0 + } + let offerUsdValue = Double(amount.amount.value) * usdRate + let giftUsdValue = Double(valueAmount) / 100.0 + + let fraction = giftUsdValue / offerUsdValue + let percentage = Int(fraction * 100) - 100 + + if percentage > 20 { + let warningText = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_BadValue("\(percentage)%").string + content.append(AnyComponentWithIdentity( + id: "warning", + component: AnyComponent( + AlertTextComponent(content: .plain(warningText), color: .destructive, textSize: .small) + ) + )) + } + } + + let updatedPresentationDataSignal = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(actionAlignment: .vertical, dismissOnOutsideTap: true, allowInputInset: false), + content: content, + actions: [ + .init(title: buttonText, type: .default, action: { + commit() + }), + .init(title: strings.Common_Cancel) + ], + updatedPresentationData: (initial: presentationData, signal: updatedPresentationDataSignal) + ) + + var dismissAllTooltipsImpl: (() -> Void)? + showAttributeInfoImpl = { [weak alertController] tag, text in + dismissAllTooltipsImpl?() + guard let alertController, let sourceView = alertController.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: alertController.view) else { + return + } + + let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize()) + let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + }) + alertController.present(tooltipController, in: .current) + } + dismissAllTooltipsImpl = { [weak alertController] in + guard let alertController else { + return + } + alertController.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + }) + alertController.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + return true + }) + } + return alertController +} + +final class AlertTransferHeaderComponent: Component { + typealias EnvironmentType = AlertComponentEnvironment + + let fromComponent: AnyComponentWithIdentity + let toComponent: AnyComponentWithIdentity + + public init( + fromComponent: AnyComponentWithIdentity, + toComponent: AnyComponentWithIdentity + ) { + self.fromComponent = fromComponent + self.toComponent = toComponent + } + + public static func ==(lhs: AlertTransferHeaderComponent, rhs: AlertTransferHeaderComponent) -> Bool { + if lhs.fromComponent != rhs.fromComponent { + return false + } + if lhs.toComponent != rhs.toComponent { + return false + } + return true + } + + public final class View: UIView { + private let from = ComponentView() + private let to = ComponentView() + private let arrow = ComponentView() + + private var component: AlertTransferHeaderComponent? + private weak var state: EmptyComponentState? + + func update(component: AlertTransferHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let size = CGSize(width: 148.0, height: 60.0) + let sideInset = floorToScreenPixels((availableSize.width - size.width) / 2.0) + + let fromSize = self.from.update( + transition: transition, + component: component.fromComponent.component, + environment: {}, + containerSize: CGSize(width: 60.0, height: 60.0) + ) + let fromFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: fromSize) + if let fromView = self.from.view { + if fromView.superview == nil { + self.addSubview(fromView) + } + transition.setFrame(view: fromView, frame: fromFrame) + } + + + let arrowSize = self.arrow.update( + transition: transition, + component: AnyComponent( + BundleIconComponent(name: "Peer Info/AlertArrow", tintColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.2)) + ), + environment: {}, + containerSize: availableSize + ) + let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - arrowSize.width) / 2.0), y: floorToScreenPixels((size.height - arrowSize.height) / 2.0)), size: arrowSize) + if let arrowView = self.arrow.view { + if arrowView.superview == nil { + self.addSubview(arrowView) + } + transition.setFrame(view: arrowView, frame: arrowFrame) + } + + let toSize = self.to.update( + transition: transition, + component: component.toComponent.component, + environment: {}, + containerSize: CGSize(width: 60.0, height: 60.0) + ) + let toFrame = CGRect(origin: CGPoint(x: availableSize.width - toSize.width - sideInset, y: 0.0), size: toSize) + if let toView = self.to.view { + if toView.superview == nil { + self.addSubview(toView) + } + transition.setFrame(view: toView, frame: toFrame) + } + + return CGSize(width: availableSize.width, height: size.height + 11.0) + } + } + + 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) + } +} + + + +final class AlertTableComponent: Component { + typealias EnvironmentType = AlertComponentEnvironment + + let items: [TableComponent.Item] + + public init( + items: [TableComponent.Item] + ) { + self.items = items + } + + public static func ==(lhs: AlertTableComponent, rhs: AlertTableComponent) -> Bool { + if lhs.items != rhs.items { + return false + } + return true + } + + public final class View: UIView { + private let table = ComponentView() + + private var component: AlertTableComponent? + private weak var state: EmptyComponentState? + + func update(component: AlertTableComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let tableSize = self.table.update( + transition: transition, + component: AnyComponent( + TableComponent( + theme: environment.theme, + items: component.items, + semiTransparent: true + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width + 20.0, height: availableSize.height) + ) + let tableFrame = CGRect(origin: CGPoint(x: -10.0, y: 5.0), size: tableSize) + if let tableView = self.table.view { + if tableView.superview == nil { + self.addSubview(tableView) + } + transition.setFrame(view: tableView, frame: tableFrame) + } + return CGSize(width: availableSize.width, height: tableSize.height + 10.0) + } + } + + 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/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift index a2e99d514c..8dfe60413b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift @@ -15,6 +15,7 @@ import ActivityIndicator import MultilineTextWithEntitiesComponent import TelegramStringFormatting import TextFormat +import AlertComponent private final class GiftRemoveInfoAlertContentNode: AlertContentNode { private let context: AccountContext diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index a58d8bf55c..aed94639bc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -6,421 +6,15 @@ 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 GiftTransferAlertContentNode: 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 - ) - ), - 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 - } -} +import AlertComponent +import TableComponent public func giftTransferAlertController( context: AccountContext, @@ -429,7 +23,7 @@ public func giftTransferAlertController( transferStars: Int64, navigationController: NavigationController?, commit: @escaping () -> Void -) -> AlertController { +) -> AlertScreen { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings @@ -444,27 +38,176 @@ public func giftTransferAlertController( buttonText = strings.Gift_Transfer_Confirmation_TransferFree } - var contentNode: GiftTransferAlertContentNode? - 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) - })] + let tableFont = Font.regular(15.0) + let tableTextColor = presentationData.theme.list.itemPrimaryTextColor - contentNode = GiftTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions) + let modelButtonTag = GenericComponentViewTag() + let backdropButtonTag = GenericComponentViewTag() + let symbolButtonTag = GenericComponentViewTag() + var showAttributeInfoImpl: ((Any, String) -> Void)? - let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: navigationController, chatPeerId: context.account.peerId, showBalance: transferStars > 0) - contentNode?.getController = { [weak controller] in - return controller + 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 gift.attributes { + attributeMap[attribute.attributeType] = attribute } - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() + + 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 = 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 = 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 = 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: context, + text: formatPercentage(percentage), + color: presentationData.theme.list.itemAccentColor + )), + action: { + showAttributeInfoImpl?(tag, 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 + )) } } - return controller + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertTransferHeaderComponent( + fromComponent: AnyComponentWithIdentity(id: "gift", component: AnyComponent( + GiftItemComponent( + context: context, + theme: presentationData.theme, + strings: strings, + peer: nil, + subject: .uniqueGift(gift: gift, price: nil), + mode: .thumbnail + ) + )), + toComponent: AnyComponentWithIdentity(id: "avatar", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: peer + ) + )) + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + content.append(AnyComponentWithIdentity( + id: "table", + component: AnyComponent( + AlertTableComponent(items: tableItems) + ) + )) + + let alertController = AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical, dismissOnOutsideTap: true, allowInputInset: false), + content: content, + actions: [ + .init(title: buttonText, type: .default, action: { + commit() + }), + .init(title: strings.Common_Cancel) + ] + ) + + var dismissAllTooltipsImpl: (() -> Void)? + showAttributeInfoImpl = { [weak alertController] tag, text in + dismissAllTooltipsImpl?() + guard let alertController, let sourceView = alertController.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: alertController.view) else { + return + } + + let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize()) + let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + }) + alertController.present(tooltipController, in: .current) + } + dismissAllTooltipsImpl = { [weak alertController] in + guard let alertController else { + return + } + alertController.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + }) + alertController.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + return true + }) + } + return alertController } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeCostScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeCostScreen.swift index d92183300d..e3c7712fb4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeCostScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeCostScreen.swift @@ -17,6 +17,7 @@ import LottieComponent import ProfileLevelRatingBarComponent import TextFormat import TelegramStringFormatting +import TableComponent private final class GiftUpgradeCostScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift similarity index 94% rename from submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift rename to submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift index 9665708ae8..b0d45858e8 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift @@ -24,24 +24,30 @@ import SegmentControlComponent import GiftAnimationComponent import GlassBackgroundComponent -private final class GiftUpgradePreviewScreenComponent: Component { +private final class GiftUpgradeVariantsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let gift: StarGift let attributes: [StarGift.UniqueGift.Attribute] + let selectedAttributes: [StarGift.UniqueGift.Attribute]? + let focusedAttribute: StarGift.UniqueGift.Attribute? init( context: AccountContext, gift: StarGift, - attributes: [StarGift.UniqueGift.Attribute] + attributes: [StarGift.UniqueGift.Attribute], + selectedAttributes: [StarGift.UniqueGift.Attribute]?, + focusedAttribute: StarGift.UniqueGift.Attribute? ) { self.context = context self.gift = gift self.attributes = attributes + self.selectedAttributes = selectedAttributes + self.focusedAttribute = focusedAttribute } - static func ==(lhs: GiftUpgradePreviewScreenComponent, rhs: GiftUpgradePreviewScreenComponent) -> Bool { + static func ==(lhs: GiftUpgradeVariantsScreenComponent, rhs: GiftUpgradeVariantsScreenComponent) -> Bool { return true } @@ -127,7 +133,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { private var ignoreScrolling: Bool = false - private var component: GiftUpgradePreviewScreenComponent? + private var component: GiftUpgradeVariantsScreenComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false private var environment: ViewControllerComponentContainer.Environment? @@ -426,7 +432,11 @@ private final class GiftUpgradePreviewScreenComponent: Component { rarity = rarityValue modelAttribute = attribute - isSelected = self.selectedModel == attribute + if case let .model(_, selectedFile, _) = self.selectedModel { + isSelected = file.fileId == selectedFile.fileId + } else { + isSelected = false + } } case let .backdrop(name, id, _, _, _, _, rarityValue): itemId += "\(id)" @@ -435,7 +445,11 @@ private final class GiftUpgradePreviewScreenComponent: Component { rarity = rarityValue backdropAttribute = attribute - isSelected = self.selectedBackdrop == attribute + if case let .backdrop(_, selectedId, _, _, _, _, _) = self.selectedBackdrop { + isSelected = id == selectedId + } else { + isSelected = false + } } case let .pattern(name, file, rarityValue): itemId += "\(file.fileId.id)" @@ -444,7 +458,11 @@ private final class GiftUpgradePreviewScreenComponent: Component { rarity = rarityValue symbolAttribute = attribute - isSelected = self.selectedSymbol == attribute + if case let .pattern(_, selectedFile, _) = self.selectedSymbol { + isSelected = file.fileId == selectedFile.fileId + } else { + isSelected = false + } } default: break @@ -557,7 +575,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { } } - func update(component: GiftUpgradePreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(component: GiftUpgradeVariantsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -626,6 +644,34 @@ private final class GiftUpgradePreviewScreenComponent: Component { }).shuffled().prefix(15)) self.previewSymbols = randomSymbols + if let selectedAttributes = component.selectedAttributes { + self.isPlaying = false + for attribute in selectedAttributes { + switch attribute { + case .model: + self.selectedModel = attribute + case .pattern: + self.selectedSymbol = attribute + case .backdrop: + self.selectedBackdrop = attribute + default: + break + } + } + } + if let focusedAttribute = component.focusedAttribute { + switch focusedAttribute { + case .model: + self.selectedSection = .models + case .pattern: + self.selectedSection = .symbols + case .backdrop: + self.selectedSection = .backdrops + default: + break + } + } + self.updateEffectiveGifts(attributes: component.attributes) } @@ -826,6 +872,16 @@ private final class GiftUpgradePreviewScreenComponent: Component { contentHeight += 16.0 + let selectedId: AnyHashable + switch self.selectedSection { + case .models: + selectedId = AnyHashable(SelectedSection.models) + case .backdrops: + selectedId = AnyHashable(SelectedSection.backdrops) + case .symbols: + selectedId = AnyHashable(SelectedSection.symbols) + } + let segmentedSize = self.segmentControl.update( transition: transition, component: AnyComponent(SegmentControlComponent( @@ -835,7 +891,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { SegmentControlComponent.Item(id: AnyHashable(SelectedSection.backdrops), title: environment.strings.Gift_Variants_Backdrops), SegmentControlComponent.Item(id: AnyHashable(SelectedSection.symbols), title: environment.strings.Gift_Variants_Symbols) ], - selectedId: "models", + selectedId: selectedId, action: { [weak self] id in guard let self, let component = self.component, let id = id.base as? SelectedSection else { return @@ -1074,7 +1130,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { } } -public class GiftUpgradePreviewScreen: ViewControllerComponentContainer { +public class GiftUpgradeVariantsScreen: ViewControllerComponentContainer { private let context: AccountContext private var didPlayAppearAnimation: Bool = false @@ -1083,14 +1139,18 @@ public class GiftUpgradePreviewScreen: ViewControllerComponentContainer { public init( context: AccountContext, gift: StarGift, - attributes: [StarGift.UniqueGift.Attribute] + attributes: [StarGift.UniqueGift.Attribute], + selectedAttributes: [StarGift.UniqueGift.Attribute]?, + focusedAttribute: StarGift.UniqueGift.Attribute? ) { self.context = context - super.init(context: context, component: GiftUpgradePreviewScreenComponent( + super.init(context: context, component: GiftUpgradeVariantsScreenComponent( context: context, gift: gift, - attributes: attributes + attributes: attributes, + selectedAttributes: selectedAttributes, + focusedAttribute: focusedAttribute ), navigationBarAppearance: .none, theme: .default) self.statusBar.statusBarStyle = .Ignore @@ -1114,7 +1174,7 @@ public class GiftUpgradePreviewScreen: ViewControllerComponentContainer { if !self.didPlayAppearAnimation { self.didPlayAppearAnimation = true - if let componentView = self.node.hostView.componentView as? GiftUpgradePreviewScreenComponent.View { + if let componentView = self.node.hostView.componentView as? GiftUpgradeVariantsScreenComponent.View { componentView.animateIn() } } @@ -1124,7 +1184,7 @@ public class GiftUpgradePreviewScreen: ViewControllerComponentContainer { if !self.isDismissed { self.isDismissed = true - if let componentView = self.node.hostView.componentView as? GiftUpgradePreviewScreenComponent.View { + if let componentView = self.node.hostView.componentView as? GiftUpgradeVariantsScreenComponent.View { componentView.animateOut(completion: { [weak self] in completion?() self?.dismiss(animated: false) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index eda1ea754b..c267b6f28a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -25,6 +25,7 @@ import GiftAnimationComponent import ContextUI import GiftItemComponent import GlassBarButtonComponent +import TableComponent private final class GiftValueSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index e21689fcac..25c68e0949 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -41,6 +41,8 @@ import ProfileLevelRatingBarComponent import AnimatedTextComponent import InfoParagraphComponent import ChatMessagePaymentAlertController +import TableComponent +import PeerTableCellComponent private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -121,6 +123,8 @@ private final class GiftViewSheetContent: CombinedComponent { let levelsDisposable = MetaDisposable() var nextGiftToUpgrade: ProfileGiftsContext.State.StarGift? + var giftVariantsDisposable = MetaDisposable() + var buyForm: BotPaymentForm? var buyFormDisposable: Disposable? var buyDisposable: Disposable? @@ -375,6 +379,7 @@ private final class GiftViewSheetContent: CombinedComponent { self.buyDisposable?.dispose() self.levelsDisposable.dispose() self.starsTopUpOptionsDisposable?.dispose() + self.giftVariantsDisposable.dispose() } func openPeer(_ peer: EnginePeer, gifts: Bool = false, dismiss: Bool = true) { @@ -1351,6 +1356,33 @@ private final class GiftViewSheetContent: CombinedComponent { }) } + + func openUpgradeVariants(attribute: StarGift.UniqueGift.Attribute? = nil) { + guard let controller = self.getController() as? GiftViewScreen, let arguments = self.subject.arguments else { + return + } + var selectedAttributes: [StarGift.UniqueGift.Attribute]? + if case let .unique(uniqueGift) = arguments.gift { + selectedAttributes = uniqueGift.attributes + } + + self.giftVariantsDisposable.set((self.context.engine.payments.getStarGiftUpgradeAttributes(giftId: arguments.gift.giftId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] attributes in + guard let self, let attributes else { + return + } + let variantsController = self.context.sharedContext.makeGiftUpgradeVariantsScreen( + context: self.context, + gift: arguments.gift, + attributes: attributes, + selectedAttributes: selectedAttributes, + focusedAttribute: attribute + ) + controller.push(variantsController) + })) + } + func showAttributeInfo(tag: Any, text: String) { guard let controller = self.getController() as? GiftViewScreen else { return @@ -2514,7 +2546,7 @@ private final class GiftViewSheetContent: CombinedComponent { let upgradeNextButton = Child(PlainButtonComponent.self) let upgradeTitle = Child(MultilineTextComponent.self) - let upgradeDescription = Child(BalancedTextComponent.self) + let upgradeDescription = Child(PlainButtonComponent.self) let upgradePerks = Child(List.self) let upgradeKeepName = Child(PlainButtonComponent.self) let upgradePriceButton = Child(PlainButtonComponent.self) @@ -2721,10 +2753,10 @@ private final class GiftViewSheetContent: CombinedComponent { } headerSubject = .unique(state.justUpgraded ? state.upgradePreview?.attributes : nil, uniqueGift) } else if state.inUpgradePreview, let attributes = state.upgradePreview?.attributes { - headerHeight = 258.0 + headerHeight = 246.0 headerSubject = .preview(attributes) } else if case let .upgradePreview(attributes, _) = component.subject { - headerHeight = 258.0 + headerHeight = 246.0 headerSubject = .preview(attributes) } else if case let .wearPreview(_, attributes) = component.subject, let attributes { headerHeight = 200.0 @@ -2978,9 +3010,9 @@ private final class GiftViewSheetContent: CombinedComponent { originY += 16.0 } else if showUpgradePreview { let title: String - let description: String + //let description: String let uniqueText: String - let transferableText: String + //let transferableText: String let tradableText: String if !incoming, case let .profileGift(peerId, _) = subject, let peer = state.peerMap[peerId] { var peerName = peer.compactDisplayTitle @@ -2988,9 +3020,9 @@ private final class GiftViewSheetContent: CombinedComponent { peerName = "\(peerName.prefix(22))…" } title = environment.strings.Gift_Upgrade_GiftTitle - description = environment.strings.Gift_Upgrade_GiftDescription(peerName).string + // description = environment.strings.Gift_Upgrade_GiftDescription(peerName).string uniqueText = strings.Gift_Upgrade_Unique_GiftDescription(peerName).string - transferableText = strings.Gift_Upgrade_Transferable_GiftDescription(peerName).string + // transferableText = strings.Gift_Upgrade_Transferable_GiftDescription(peerName).string tradableText = strings.Gift_Upgrade_Tradable_GiftDescription(peerName).string } else if case let .upgradePreview(_, peerName) = component.subject { var peerName = peerName @@ -2998,15 +3030,15 @@ private final class GiftViewSheetContent: CombinedComponent { peerName = "\(peerName.prefix(22))…" } title = environment.strings.Gift_Upgrade_IncludeTitle - description = environment.strings.Gift_Upgrade_IncludeDescription(peerName).string + // description = environment.strings.Gift_Upgrade_IncludeDescription(peerName).string uniqueText = strings.Gift_Upgrade_Unique_IncludeDescription - transferableText = strings.Gift_Upgrade_Transferable_IncludeDescription + // transferableText = strings.Gift_Upgrade_Transferable_IncludeDescription tradableText = strings.Gift_Upgrade_Tradable_IncludeDescription } else { title = environment.strings.Gift_Upgrade_Title - description = environment.strings.Gift_Upgrade_Description + // description = environment.strings.Gift_Upgrade_Description uniqueText = strings.Gift_Upgrade_Unique_Description - transferableText = strings.Gift_Upgrade_Transferable_Description + // transferableText = strings.Gift_Upgrade_Transferable_Description tradableText = strings.Gift_Upgrade_Tradable_Description } @@ -3024,39 +3056,109 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) - let upgradeDescription = upgradeDescription.update( - component: BalancedTextComponent( - text: .plain(NSAttributedString( - string: description, - font: Font.regular(13.0), - textColor: .white, - paragraphAlignment: .center - )), - horizontalAlignment: .center, - maximumNumberOfLines: 5, - lineSpacing: 0.2, - tintColor: vibrantColor - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), - transition: context.transition - ) - let spacing: CGFloat = 6.0 - let totalHeight: CGFloat = upgradeTitle.size.height + spacing + upgradeDescription.size.height - - headerComponents.append({ - context.add(upgradeTitle - .position(CGPoint(x: context.availableSize.width / 2.0, y: floor(212.0 - totalHeight / 2.0 + upgradeTitle.size.height / 2.0))) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) + if case let .generic(gift) = component.subject.arguments?.gift, let upgradePreview = state.upgradePreview { + var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + + var i = 0 + for attribute in upgradePreview.attributes { + if case .model = attribute { + switch i { + case 0: + variant1 = .preview(attributes: [attribute], rarity: 0) + case 1: + variant2 = .preview(attributes: [attribute], rarity: 0) + case 2: + variant3 = .preview(attributes: [attribute], rarity: 0) + default: + break + } + i += 1 + } + } + //TODO:localize + var buttonColor: UIColor = UIColor.white.withAlphaComponent(0.16) + if let previewPatternColor = giftCompositionExternalState.previewPatternColor { + buttonColor = previewPatternColor + } + let upgradeDescription = upgradeDescription.update( + component: PlainButtonComponent( + content: AnyComponent( + ZStack([ + AnyComponentWithIdentity(id: "background", component: AnyComponent( + FilledRoundedRectangleComponent(color: buttonColor, cornerRadius: .minEdge, smoothCorners: false) + )), + AnyComponentWithIdentity(id: "label", component: AnyComponent(HStack([ + AnyComponentWithIdentity(id: "icon1", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant1, + isPlaceholder: false, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "icon2", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant2, + isPlaceholder: false, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "icon3", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant3, + isPlaceholder: false, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "View all variants", font: Font.semibold(13.0), textColor: .white))) + )), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent( + BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: .white) + )) + ], spacing: 3.0))) + ]) + ), + action: { [weak state] in + state?.openUpgradeVariants() + }, + animateScale: false + ), + availableSize: CGSize(width: 190.0, height: 24.0), + transition: context.transition ) + + let spacing: CGFloat = 6.0 + let totalHeight: CGFloat = upgradeTitle.size.height + spacing + upgradeDescription.size.height - context.add(upgradeDescription - .position(CGPoint(x: context.availableSize.width / 2.0, y: floor(212.0 + totalHeight / 2.0 - upgradeDescription.size.height / 2.0))) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - }) + headerComponents.append({ + context.add(upgradeTitle + .position(CGPoint(x: context.availableSize.width / 2.0, y: floor(194.0 - totalHeight / 2.0 + upgradeTitle.size.height / 2.0))) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + + context.add(upgradeDescription + .position(CGPoint(x: context.availableSize.width / 2.0, y: floor(198.0 + totalHeight / 2.0 - upgradeDescription.size.height / 2.0))) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + }) + } originY += 24.0 let textColor = theme.actionSheet.primaryTextColor @@ -3078,20 +3180,20 @@ private final class GiftViewSheetContent: CombinedComponent { )) ) ) - items.append( - AnyComponentWithIdentity( - id: "transferable", - component: AnyComponent(InfoParagraphComponent( - title: strings.Gift_Upgrade_Transferable_Title, - titleColor: textColor, - text: transferableText, - textColor: secondaryTextColor, - accentColor: linkColor, - iconName: "Premium/Collectible/Transferable", - iconColor: linkColor - )) - ) - ) +// items.append( +// AnyComponentWithIdentity( +// id: "transferable", +// component: AnyComponent(InfoParagraphComponent( +// title: strings.Gift_Upgrade_Transferable_Title, +// titleColor: textColor, +// text: transferableText, +// textColor: secondaryTextColor, +// accentColor: linkColor, +// iconName: "Premium/Collectible/Transferable", +// iconColor: linkColor +// )) +// ) +// ) items.append( AnyComponentWithIdentity( id: "tradable", @@ -3106,6 +3208,21 @@ private final class GiftViewSheetContent: CombinedComponent { )) ) ) + //TODO:localize + items.append( + AnyComponentWithIdentity( + id: "wearable", + component: AnyComponent(InfoParagraphComponent( + title: "Wearable", + titleColor: textColor, + text: "Display gifts on your page and set them as profile covers or statuses.", + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/Collectible/Transferable", + iconColor: linkColor + )) + ) + ) let perksSideInset = sideInset + 16.0 let upgradePerks = upgradePerks.update( @@ -3598,7 +3715,7 @@ private final class GiftViewSheetContent: CombinedComponent { id: AnyHashable(0), component: AnyComponent(Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, strings: strings, @@ -3631,7 +3748,7 @@ private final class GiftViewSheetContent: CombinedComponent { } else { ownerComponent = AnyComponent(Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, strings: strings, @@ -3682,7 +3799,7 @@ private final class GiftViewSheetContent: CombinedComponent { title: strings.Gift_Unique_Telegram, component: AnyComponent(Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, strings: strings, @@ -3712,7 +3829,7 @@ private final class GiftViewSheetContent: CombinedComponent { id: AnyHashable(0), component: AnyComponent(Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, strings: strings, @@ -3742,7 +3859,7 @@ private final class GiftViewSheetContent: CombinedComponent { } else { fromComponent = AnyComponent(Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, strings: strings, @@ -3767,7 +3884,7 @@ private final class GiftViewSheetContent: CombinedComponent { id: "from_anon", title: strings.Gift_View_From, component: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, strings: strings, @@ -4151,7 +4268,7 @@ private final class GiftViewSheetContent: CombinedComponent { color: theme.list.itemAccentColor )), action: { [weak state] in - state?.showAttributeInfo(tag: tag, text: strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string) + state?.openUpgradeVariants(attribute: attribute) } ).tagged(tag)) )) @@ -5784,115 +5901,6 @@ func formatPercentage(_ value: Float) -> String { return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + "%" } -final class PeerCellComponent: Component { - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let peer: EnginePeer? - - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer?) { - self.context = context - self.theme = theme - self.strings = strings - self.peer = peer - } - - static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.peer != rhs.peer { - return false - } - return true - } - - final class View: UIView { - private let avatarNode: AvatarNode - private let text = ComponentView() - - private var component: PeerCellComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) - - super.init(frame: frame) - - self.addSubnode(self.avatarNode) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - self.state = state - - let avatarSize = CGSize(width: 22.0, height: 22.0) - let spacing: CGFloat = 6.0 - - var peerName: String - let avatarOverride: AvatarNodeImageOverride? - if let peerValue = component.peer { - peerName = peerValue.compactDisplayTitle - if peerName.count > 40 { - peerName = "\(peerName.prefix(40))…" - } - avatarOverride = nil - } else { - peerName = component.strings.Gift_View_HiddenName - avatarOverride = .anonymousSavedMessagesIcon(isColored: true) - } - - let avatarNaturalSize = CGSize(width: 40.0, height: 40.0) - self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride) - self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize) - - let textSize = self.text.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.peer != nil ? component.theme.list.itemAccentColor : component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)) - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height) - ) - - let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) - - let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) - self.avatarNode.frame = avatarFrame - - if let view = self.text.view { - if view.superview == nil { - self.addSubview(view) - } - let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) - transition.setFrame(view: view, frame: textFrame) - } - - 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) - } -} - final class HeaderContentComponent: Component { let attributedText: NSAttributedString @@ -6250,6 +6258,10 @@ final class AvatarComponent: Component { } func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + self.avatarNode.font = avatarPlaceholderFont(size: 42.0 * floor(availableSize.width / 100.0)) + } + self.component = component self.state = state diff --git a/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD new file mode 100644 index 0000000000..363e3ee0c3 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerTableCellComponent", + module_name = "PeerTableCellComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramCore", + "//submodules/Components/MultilineTextComponent", + "//submodules/AccountContext", + "//submodules/AvatarNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/Sources/PeerTableCellComponent.swift b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/Sources/PeerTableCellComponent.swift new file mode 100644 index 0000000000..6ff8042f6d --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/Sources/PeerTableCellComponent.swift @@ -0,0 +1,123 @@ +import Foundation +import UIKit +import ComponentFlow +import Display +import TelegramCore +import TelegramPresentationData +import MultilineTextComponent +import AvatarNode +import AccountContext + +public final class PeerTableCellComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer? + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer? + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + } + + public static func ==(lhs: PeerTableCellComponent, rhs: PeerTableCellComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + public final class View: UIView { + private let avatarNode: AvatarNode + private let text = ComponentView() + + private var component: PeerTableCellComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerTableCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let avatarSize = CGSize(width: 22.0, height: 22.0) + let spacing: CGFloat = 6.0 + + var peerName: String + let avatarOverride: AvatarNodeImageOverride? + if let peerValue = component.peer { + peerName = peerValue.compactDisplayTitle + if peerName.count > 40 { + peerName = "\(peerName.prefix(40))…" + } + avatarOverride = nil + } else { + peerName = component.strings.Gift_View_HiddenName + avatarOverride = .anonymousSavedMessagesIcon(isColored: true) + } + + let avatarNaturalSize = CGSize(width: 40.0, height: 40.0) + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride) + self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.peer != nil ? component.theme.list.itemAccentColor : component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height) + ) + + let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) + + let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) + self.avatarNode.frame = avatarFrame + + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + transition.setFrame(view: view, frame: textFrame) + } + + 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/Gifts/TableComponent/BUILD b/submodules/TelegramUI/Components/Gifts/TableComponent/BUILD new file mode 100644 index 0000000000..54dc0f8a77 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/TableComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TableComponent", + module_name = "TableComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/Components/MultilineTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift b/submodules/TelegramUI/Components/Gifts/TableComponent/Sources/TableComponent.swift similarity index 93% rename from submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift rename to submodules/TelegramUI/Components/Gifts/TableComponent/Sources/TableComponent.swift index d696aef50f..af189d8f10 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/TableComponent/Sources/TableComponent.swift @@ -5,9 +5,9 @@ import Display import TelegramPresentationData import MultilineTextComponent -final class TableComponent: CombinedComponent { - class Item: Equatable { - enum TitleFont { +public final class TableComponent: CombinedComponent { + public class Item: Equatable { + public enum TitleFont { case regular case bold } @@ -19,7 +19,14 @@ final class TableComponent: CombinedComponent { public let component: AnyComponent public let insets: UIEdgeInsets? - public init(id: IdType, title: String?, titleFont: TitleFont = .regular, hasBackground: Bool = false, component: AnyComponent, insets: UIEdgeInsets? = nil) { + public init( + id: IdType, + title: String?, + titleFont: TitleFont = .regular, + hasBackground: Bool = false, + component: AnyComponent, + insets: UIEdgeInsets? = nil + ) { self.id = AnyHashable(id) self.title = title self.titleFont = titleFont @@ -74,13 +81,13 @@ final class TableComponent: CombinedComponent { return true } - final class State: ComponentState { + public final class State: ComponentState { var cachedLastBackgroundImage: (UIImage, PresentationTheme)? var cachedLeftColumnImage: (UIImage, PresentationTheme)? var cachedBorderImage: (UIImage, PresentationTheme)? } - func makeState() -> State { + public func makeState() -> State { return State() } @@ -98,11 +105,15 @@ final class TableComponent: CombinedComponent { let horizontalPadding: CGFloat = 12.0 let borderWidth: CGFloat = 1.0 - let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor - let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) - var secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor + let borderColor: UIColor + let secondaryBackgroundColor: UIColor if context.component.semiTransparent { - secondaryBackgroundColor = borderColor.withMultipliedAlpha(0.5) + borderColor = context.component.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1) + secondaryBackgroundColor = context.component.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.05) + } else { + let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor + borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) + secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor } var leftColumnWidth: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift index 401f0ffee7..7f327300a6 100644 --- a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift +++ b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift @@ -66,17 +66,17 @@ public final class LiquidLensView: UIView { private struct Params: Equatable { var size: CGSize - var selectionX: CGFloat - var selectionWidth: CGFloat + var selectionOrigin: CGPoint + var selectionSize: CGSize var inset: CGFloat var isDark: Bool var isLifted: Bool var isCollapsed: Bool - init(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, inset: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { + init(size: CGSize, selectionOrigin: CGPoint, selectionSize: CGSize, inset: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { self.size = size - self.selectionX = selectionX - self.selectionWidth = selectionWidth + self.selectionOrigin = selectionOrigin + self.selectionSize = selectionSize self.inset = inset self.isLifted = isLifted self.isDark = isDark @@ -121,12 +121,12 @@ public final class LiquidLensView: UIView { private var liftedDisplayLink: SharedDisplayLinkDriver.Link? - public var selectionX: CGFloat? { - return self.params?.selectionX + public var selectionOrigin: CGPoint? { + return self.params?.selectionOrigin } - public var selectionWidth: CGFloat? { - return self.params?.selectionWidth + public var selectionSize: CGSize? { + return self.params?.selectionSize } public private(set) var isAnimating: Bool = false { @@ -286,8 +286,8 @@ public final class LiquidLensView: UIView { lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: view) } - public func update(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, inset: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { - let params = Params(size: size, selectionX: selectionX, selectionWidth: selectionWidth, inset: inset, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) + public func update(size: CGSize, selectionOrigin: CGPoint, selectionSize: CGSize, inset: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { + let params = Params(size: size, selectionOrigin: selectionOrigin, selectionSize: selectionSize, inset: inset, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) if self.params == params { return } @@ -444,7 +444,7 @@ public final class LiquidLensView: UIView { transition.setCornerRadius(layer: self.liftedContainerView.layer, cornerRadius: params.size.height * 0.5) } - let baseLensFrame = CGRect(origin: CGPoint(x: params.selectionX, y: 0.0), size: CGSize(width: params.selectionWidth, height: params.size.height)) + let baseLensFrame = CGRect(origin: CGPoint(x: params.selectionOrigin.x, y: 0.0), size: CGSize(width: params.selectionSize.width, height: params.size.height)) self.updateLens(params: LensParams(baseFrame: baseLensFrame, inset: params.inset, isLifted: params.isLifted), transition: transition) if let legacyContentMaskView = self.legacyContentMaskView { diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index 9f933e24f4..deef216eae 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -1170,6 +1170,7 @@ final class AffiliateProgramSetupScreenComponent: Component { transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) @@ -1186,7 +1187,7 @@ final class AffiliateProgramSetupScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - bottomPanelButtonInsets.left - bottomPanelButtonInsets.right, height: 50.0) + containerSize: CGSize(width: availableSize.width - bottomPanelButtonInsets.left - bottomPanelButtonInsets.right, height: 52.0) ) let bottomPanelHeight: CGFloat = bottomPanelButtonInsets.top + bottomPanelButtonSize.height + bottomPanelButtonInsets.bottom + bottomPanelTextSize.height + 8.0 + environment.safeInsets.bottom diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift index d8d473f822..3df157c9fd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift @@ -1130,11 +1130,11 @@ private final class JoinAffiliateProgramScreenComponent: Component { )), background: AnyComponent(FilledRoundedRectangleComponent( color: environment.theme.list.itemInputField.backgroundColor, - cornerRadius: .value(8.0), + cornerRadius: .minEdge, smoothCorners: true )), effectAlignment: .center, - minSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0), + minSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 52.0), contentInsets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0), action: { [weak self] in guard let self, case let .active(active) = self.currentMode else { @@ -1148,7 +1148,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { animateContents: false )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 52.0) ) let linkTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - linkTextSize.width) * 0.5), y: contentHeight), size: linkTextSize) if let linkTextView = self.linkText.view { @@ -1170,6 +1170,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { actionButtonTitle = environment.strings.AffiliateProgram_ActionCopyLink } + let buttonSideInset: CGFloat = 30.0 let actionButtonSize = self.actionButton.update( transition: transition, component: AnyComponent(ButtonComponent( @@ -1208,7 +1209,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - 30.0 * 2.0, height: 52.0) + containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) ) let bottomTextSize = self.bottomText.update( @@ -1238,7 +1239,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight)) transition.setFrame(view: self.bottomPanelContainer, frame: bottomPanelFrame) - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: actionButtonSize) + let actionButtonFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: 0.0), size: actionButtonSize) if let actionButtonView = self.actionButton.view { if actionButtonView.superview == nil { self.bottomPanelContainer.addSubview(actionButtonView) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift index bd56a1ee54..f72dcb9500 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -308,7 +308,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) - var topOffset = 10.0 + var topOffset = 15.0 let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: labelSize) if labelSize.height > 0.0 { topOffset += labelSize.height diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD index 4166ff8413..d0bc74abb5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Markdown", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/Components/BundleIconComponent", "//submodules/TextFormat", "//submodules/TelegramUI/Components/ListSectionComponent", @@ -32,6 +33,8 @@ swift_library( "//submodules/TelegramStringFormatting", "//submodules/TelegramUI/Components/ListItemComponentAdaptor", "//submodules/TelegramUI/Components/PeerInfo/MessagePriceItem", + "//submodules/UndoUI", + "//submodules/ShareController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift index be77701a7b..858ba015a3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift @@ -22,6 +22,10 @@ import Markdown import TelegramStringFormatting import MessagePriceItem import ListItemComponentAdaptor +import ButtonComponent +import PlainButtonComponent +import UndoUI +import ShareController final class PostSuggestionsSettingsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -68,6 +72,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { private let subtitle = ComponentView() private let switchSection = ComponentView() private let contentSection = ComponentView() + private let linkSection = ComponentView() private var isUpdating: Bool = false @@ -166,6 +171,91 @@ final class PostSuggestionsSettingsScreenComponent: Component { } } + func dismissAllTooltips() { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + } + + func copyLink(_ link: String) { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + UIPasteboard.general.string = link + + self.dismissAllTooltips() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + + func shareLink(_ link: String) { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + + let context = component.context + let shareController = ShareController(context: context, subject: .url(link), updatedPresentationData: nil) + shareController.completed = { [weak controller] peerIds in + let _ = (context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).start(next: { [weak controller] peerList in + let peers = peerList.compactMap { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak controller] peer in + guard let peer else { + return + } + guard let navigationController = controller?.navigationController as? NavigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .window(.root)) + }) + } + shareController.actionCompleted = { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + controller.present(shareController, in: .window(.root)) + } + func update(component: PostSuggestionsSettingsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -429,6 +519,50 @@ final class PostSuggestionsSettingsScreenComponent: Component { if self.areSuggestionsEnabled { contentHeight += contentSectionSize.height + contentHeight += sectionSpacing + } + + let address = component.peer?.addressName ?? "" + let link = "t.me/\(address)?direct" + let fullLink = "https://\(link)" + var linkSectionItems: [AnyComponentWithIdentity] = [] + linkSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent( + LinkComponent( + theme: environment.theme, + strings: environment.strings, + link: link, + copyAction: { [weak self] in + self?.copyLink(fullLink) + }, + shareAction: { [weak self] in + self?.shareLink(fullLink) + } + ) + ))) + let linkSectionSize = self.linkSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + style: .glass, + header: nil, + footer: nil, + items: linkSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let linkSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: linkSectionSize) + if let linkSectionView = self.linkSection.view { + if linkSectionView.superview == nil { + self.scrollView.addSubview(linkSectionView) + self.linkSection.parentState = state + } + transition.setFrame(view: linkSectionView, frame: linkSectionFrame) + alphaTransition.setAlpha(view: linkSectionView, alpha: self.areSuggestionsEnabled && !address.isEmpty ? 1.0 : 0.0) + } + if self.areSuggestionsEnabled && !address.isEmpty { + contentHeight += switchSectionSize.height + contentHeight += sectionSpacing } contentHeight += bottomContentInset @@ -546,3 +680,323 @@ public final class PostSuggestionsSettingsScreen: ViewControllerComponentContain super.containerLayoutUpdated(layout, transition: transition) } } + + +private final class LinkContentComponent: Component { + let theme: PresentationTheme + let link: String + + init( + theme: PresentationTheme, + link: String + ) { + self.theme = theme + self.link = link + } + + static func ==(lhs: LinkContentComponent, rhs: LinkContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.link != rhs.link { + return false + } + return true + } + + final class View: UIView { + private var component: LinkContentComponent? + + private let background = ComponentView() + private let link = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: LinkContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let padding: CGFloat = 10.0 + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent( + FilledRoundedRectangleComponent( + color: component.theme.list.itemInputField.backgroundColor, + cornerRadius: .minEdge, + smoothCorners: false + ) + ), + environment: {}, + containerSize: availableSize + ) + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let linkFont = Font.regular(17.0) + let linkSize = self.link.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.link, font: linkFont, textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - padding * 4.0, height: availableSize.height) + ) + let linkFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - linkSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - linkSize.height) / 2.0) - UIScreenPixel), size: linkSize) + if let linkView = self.link.view { + if linkView.superview == nil { + self.addSubview(linkView) + } + transition.setFrame(view: linkView, frame: linkFrame) + } + + 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) + } +} + +private final class LinkComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let link: String + let copyAction: () -> Void + let shareAction: () -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + link: String, + copyAction: @escaping () -> Void, + shareAction: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.link = link + self.copyAction = copyAction + self.shareAction = shareAction + } + + static func ==(lhs: LinkComponent, rhs: LinkComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.link != rhs.link { + return false + } + return true + } + + final class View: UIView { + private let linkButton = ComponentView() + private let moreButton = ComponentView() + private var copyButton = ComponentView() + private var shareButton = ComponentView() + + private var component: LinkComponent? + private weak var state: EmptyComponentState? + + private var cachedMoreImage: (UIImage, PresentationTheme)? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: LinkComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let sideInset: CGFloat = 16.0 + var contentHeight: CGFloat = sideInset + + let linkButtonSize = self.linkButton.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent(LinkContentComponent(theme: component.theme, link: component.link)), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.copyAction() + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 52.0) + ) + let linkButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: linkButtonSize) + if let linkButtonView = self.linkButton.view { + if linkButtonView.superview == nil { + self.addSubview(linkButtonView) + } + linkButtonView.frame = linkButtonFrame + } + + let moreButtonImage: UIImage + if let (image, theme) = self.cachedMoreImage, theme === component.theme { + moreButtonImage = image + } else { + moreButtonImage = actionButtonImage(color: component.theme.list.itemInputField.controlColor)! + self.cachedMoreImage = (moreButtonImage, component.theme) + } + + let moreButtonSize = self.moreButton.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent(Image(image: moreButtonImage, contentMode: .center)), + minSize: CGSize(width: 52.0, height: 52.0), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.copyAction() + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: 52.0, height: 52.0) + ) + let moreButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - moreButtonSize.width, y: contentHeight), size: moreButtonSize) + if let moreButtonView = self.moreButton.view { + if moreButtonView.superview == nil { + self.addSubview(moreButtonView) + } + moreButtonView.frame = moreButtonFrame + } + + contentHeight += linkButtonSize.height + contentHeight += 10.0 + + var buttonWidth = availableSize.width - sideInset * 2.0 + buttonWidth = (buttonWidth - 10.0) / 2.0 + + let copyButtonSize = self.copyButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Copy", font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.copyAction() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: 52.0) + ) + let copyButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: copyButtonSize) + if let copyButtonView = self.copyButton.view { + if copyButtonView.superview == nil { + self.addSubview(copyButtonView) + } + copyButtonView.frame = copyButtonFrame + } + + let shareButtonSize = self.shareButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Share", font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.shareAction() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: 52.0) + ) + let shareButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - shareButtonSize.width, y: contentHeight), size: shareButtonSize) + if let shareButtonView = self.shareButton.view { + if shareButtonView.superview == nil { + self.addSubview(shareButtonView) + } + shareButtonView.frame = shareButtonFrame + } + + contentHeight += copyButtonSize.height + contentHeight += sideInset + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + 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) + } +} + +func stringForRemainingTime(_ duration: Int32) -> String { + let hours = duration / 3600 + let minutes = duration / 60 % 60 + let seconds = duration % 60 + let durationString: String + if hours > 0 { + durationString = String(format: "%d:%02d", hours, minutes) + } else { + durationString = String(format: "%02d:%02d", minutes, seconds) + } + return durationString +} + +private func actionButtonImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setBlendMode(.clear) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 4.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0))) + }) +} diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD index c83d5538a0..210ac682a5 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD @@ -34,6 +34,7 @@ swift_library( "//submodules/AvatarNode", "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", + "//submodules/TelegramUI/Components/AlertComponent", "//submodules/ShimmerEffect", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index c7aa5fd0ba..38e5a4afd8 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -23,6 +23,7 @@ import LottieComponent import Markdown import PeerListItemComponent import AvatarNode +import AlertComponent private let checkIcon: UIImage = { return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in @@ -527,14 +528,20 @@ final class ChatbotSetupScreenComponent: Component { } else { text = environment.strings.ChatbotSetup_Gift_Warning_CombinedText(botUsername).string } - let alertController = textAlertController(context: component.context, title: environment.strings.ChatbotSetup_Gift_Warning_Title, text: text, actions: [ - TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { - completion(false) - }), - TextAlertAction(type: .defaultAction, title: environment.strings.ChatbotSetup_Gift_Warning_Proceed, action: { - completion(true) - }) - ], parseMarkdown: true) + + let alertController = AlertScreen( + context: component.context, + title: environment.strings.ChatbotSetup_Gift_Warning_Title, + text: text, + actions: [ + .init(title: environment.strings.Common_Cancel, action: { + completion(false) + }), + .init(title: environment.strings.Common_Cancel, type: .default, action: { + completion(true) + }), + ] + ) alertController.dismissed = { byOutsideTap in if byOutsideTap { completion(false) diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift index 8ad146fd48..4e3dd73d58 100644 --- a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift @@ -522,7 +522,7 @@ private final class CollectibleItemInfoScreenContentComponent: Component { if environment.safeInsets.bottom.isZero { contentHeight += 16.0 } else { - contentHeight += environment.safeInsets.bottom + 14.0 + contentHeight += environment.safeInsets.bottom + 1.0 } return CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD index 1f4cce9b1f..d348f0bdf5 100644 --- a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD @@ -23,6 +23,8 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramCore", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift index d712e9ea04..56e8473c6c 100644 --- a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift +++ b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift @@ -14,6 +14,7 @@ import TelegramStringFormatting import BundleIconComponent import TelegramCore import TelegramPresentationData +import GlassBarButtonComponent private final class BalanceNeededSheetContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -49,9 +50,7 @@ private final class BalanceNeededSheetContentComponent: Component { private var component: BalanceNeededSheetContentComponent? private weak var state: EmptyComponentState? - - private var cachedCloseImage: (UIImage, PresentationTheme)? - + override init(frame: CGRect) { super.init(frame: frame) } @@ -71,28 +70,27 @@ private final class BalanceNeededSheetContentComponent: Component { let sideInset: CGFloat = 16.0 - let closeImage: UIImage - if let (image, theme) = self.cachedCloseImage, theme === environment.theme { - closeImage = image - } else { - closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! - self.cachedCloseImage = (closeImage, environment.theme) - } let closeButtonSize = self.closeButton.update( transition: .immediate, - component: AnyComponent(Button( - content: AnyComponent(Image(image: closeImage)), - action: { [weak self] in - guard let self, let component = self.component else { - return - } + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: environment.theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in component.dismiss() } )), environment: {}, - containerSize: CGSize(width: 30.0, height: 30.0) + containerSize: CGSize(width: 40.0, height: 40.0) ) - let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - closeButtonSize.width - 16.0, y: 12.0), size: closeButtonSize) + let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize) if let closeButtonView = self.closeButton.view { if closeButtonView.superview == nil { self.addSubview(closeButtonView) @@ -164,10 +162,12 @@ private final class BalanceNeededSheetContentComponent: Component { contentHeight += textSize.height contentHeight += 24.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonSize = self.button.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) @@ -187,9 +187,9 @@ private final class BalanceNeededSheetContentComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) - let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { self.addSubview(buttonView) @@ -197,13 +197,8 @@ private final class BalanceNeededSheetContentComponent: Component { transition.setFrame(view: buttonView, frame: buttonFrame) } contentHeight += buttonSize.height - - if environment.safeInsets.bottom.isZero { - contentHeight += 16.0 - } else { - contentHeight += environment.safeInsets.bottom + 8.0 - } - + contentHeight += buttonInsets.bottom + return CGSize(width: availableSize.width, height: contentHeight) } } @@ -307,6 +302,7 @@ private final class BalanceNeededScreenComponent: Component { }) } )), + style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: self.sheetAnimateOut )), diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD index 48c14fcad0..647e3f90c9 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD @@ -39,6 +39,8 @@ swift_library( "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/Gifts/TableComponent", + "//submodules/TelegramUI/Components/Gifts/PeerTableCellComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 9779a85641..eff457d8f6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -28,6 +28,8 @@ import MiniAppListScreen import PremiumStarComponent import GiftAnimationComponent import GlassBarButtonComponent +import TableComponent +import PeerTableCellComponent private final class StarsTransactionSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -936,9 +938,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { component: AnyComponent( Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, + strings: strings, peer: nil ) ), @@ -983,9 +986,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { id: AnyHashable(0), component: AnyComponent(Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, + strings: strings, peer: toPeer ) ), @@ -1026,9 +1030,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { toComponent = AnyComponent( Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, + strings: strings, peer: toPeer ) ), @@ -1164,9 +1169,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { component: AnyComponent( Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, + strings: strings, peer: toPeer ) ), @@ -1196,9 +1202,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { component: AnyComponent( Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, + strings: strings, peer: starRefPeer ) ), @@ -1227,9 +1234,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { component: AnyComponent( Button( content: AnyComponent( - PeerCellComponent( + PeerTableCellComponent( context: component.context, theme: theme, + strings: strings, peer: toPeer ) ), @@ -2146,241 +2154,6 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { } } -private final class TableComponent: CombinedComponent { - class Item: Equatable { - public let id: AnyHashable - public let title: String - public let component: AnyComponent - public let insets: UIEdgeInsets? - - public init(id: IdType, title: String, component: AnyComponent, insets: UIEdgeInsets? = nil) { - self.id = AnyHashable(id) - self.title = title - self.component = component - self.insets = insets - } - - public static func == (lhs: Item, rhs: Item) -> Bool { - if lhs.id != rhs.id { - return false - } - if lhs.title != rhs.title { - return false - } - if lhs.component != rhs.component { - return false - } - if lhs.insets != rhs.insets { - return false - } - return true - } - } - - private let theme: PresentationTheme - private let items: [Item] - - public init(theme: PresentationTheme, items: [Item]) { - self.theme = theme - self.items = items - } - - public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.items != rhs.items { - return false - } - return true - } - - final class State: ComponentState { - var cachedLeftColumnImage: (UIImage, PresentationTheme)? - var cachedBorderImage: (UIImage, PresentationTheme)? - } - - func makeState() -> State { - return State() - } - - public static var body: Body { - let leftColumnBackground = Child(Image.self) - let verticalBorder = Child(Rectangle.self) - let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) - let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) - let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) - let outerBorder = Child(Image.self) - - return { context in - let verticalPadding: CGFloat = 11.0 - let horizontalPadding: CGFloat = 12.0 - let borderWidth: CGFloat = 1.0 - let borderRadius: CGFloat = 14.0 - - 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 leftColumnWidth: CGFloat = 0.0 - - var updatedTitleChildren: [_UpdatedChildComponent] = [] - var updatedValueChildren: [(_UpdatedChildComponent, UIEdgeInsets)] = [] - var updatedBorderChildren: [_UpdatedChildComponent] = [] - - for item in context.component.items { - let titleChild = titleChildren[item.id].update( - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: item.title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) - )), - availableSize: context.availableSize, - transition: context.transition - ) - updatedTitleChildren.append(titleChild) - - if titleChild.size.width > leftColumnWidth { - leftColumnWidth = titleChild.size.width - } - } - - leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0) - let rightColumnWidth = context.availableSize.width - leftColumnWidth - - var i = 0 - var rowHeights: [Int: CGFloat] = [:] - var totalHeight: CGFloat = 0.0 - - for item in context.component.items { - let titleChild = updatedTitleChildren[i] - - let insets: UIEdgeInsets - if let customInsets = item.insets { - insets = customInsets - } else { - insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) - } - let valueChild = valueChildren[item.id].update( - component: item.component, - availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height), - transition: context.transition - ) - updatedValueChildren.append((valueChild, insets)) - - let rowHeight = max(40.0, max(titleChild.size.height, valueChild.size.height) + verticalPadding * 2.0) - rowHeights[i] = rowHeight - totalHeight += rowHeight - - if i < context.component.items.count - 1 { - let borderChild = borderChildren[item.id].update( - component: AnyComponent(Rectangle(color: borderColor)), - availableSize: CGSize(width: context.availableSize.width, height: borderWidth), - transition: context.transition - ) - updatedBorderChildren.append(borderChild) - } - - i += 1 - } - - let leftColumnImage: UIImage - if let (currentImage, theme) = context.state.cachedLeftColumnImage, theme === context.component.theme { - leftColumnImage = currentImage - } else { - leftColumnImage = generateImage(CGSize(width: borderRadius * 2.0 + 4.0, height: borderRadius * 2.0 + 4.0), rotatedContext: { size, context in - let bounds = CGRect(origin: .zero, size: CGSize(width: size.width + borderRadius, height: size.height)) - context.clear(bounds) - - let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) - context.setFillColor(secondaryBackgroundColor.cgColor) - context.addPath(path) - context.fillPath() - })!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius)) - context.state.cachedLeftColumnImage = (leftColumnImage, context.component.theme) - } - - let leftColumnBackground = leftColumnBackground.update( - component: Image(image: leftColumnImage), - availableSize: CGSize(width: leftColumnWidth, height: totalHeight), - transition: context.transition - ) - context.add( - leftColumnBackground - .position(CGPoint(x: leftColumnWidth / 2.0, y: totalHeight / 2.0)) - ) - - let borderImage: UIImage - if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme { - borderImage = currentImage - } else { - borderImage = generateImage(CGSize(width: borderRadius * 2.0 + 4.0, height: borderRadius * 2.0 + 4.0), rotatedContext: { size, context in - let bounds = CGRect(origin: .zero, size: size) - context.clear(bounds) - - let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) - context.setBlendMode(.clear) - context.addPath(path) - context.fillPath() - - context.setBlendMode(.normal) - context.setStrokeColor(borderColor.cgColor) - context.setLineWidth(borderWidth) - context.addPath(path) - context.strokePath() - })!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius)) - context.state.cachedBorderImage = (borderImage, context.component.theme) - } - - let outerBorder = outerBorder.update( - component: Image(image: borderImage), - availableSize: CGSize(width: context.availableSize.width, height: totalHeight), - transition: context.transition - ) - context.add(outerBorder - .position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0)) - ) - - let verticalBorder = verticalBorder.update( - component: Rectangle(color: borderColor), - availableSize: CGSize(width: borderWidth, height: totalHeight), - transition: context.transition - ) - context.add( - verticalBorder - .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: totalHeight / 2.0)) - ) - - i = 0 - var originY: CGFloat = 0.0 - for (titleChild, (valueChild, valueInsets)) in zip(updatedTitleChildren, updatedValueChildren) { - let rowHeight = rowHeights[i] ?? 0.0 - - let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) - let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + valueInsets.left, y: originY + verticalPadding), size: valueChild.size) - - context.add(titleChild - .position(titleFrame.center) - ) - - context.add(valueChild - .position(valueFrame.center) - ) - - if i < updatedBorderChildren.count { - let borderChild = updatedBorderChildren[i] - context.add(borderChild - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0)) - ) - } - - originY += rowHeight - i += 1 - } - - return CGSize(width: context.availableSize.width, height: totalHeight) - } - } -} - private final class PeerCellComponent: Component { let context: AccountContext let theme: PresentationTheme diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 8db9bfc18b..c746dae571 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -167,7 +167,11 @@ private final class SheetContent: CombinedComponent { maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxStarsAmount, nanos: 0) case .ton: amountTitle = environment.strings.Stars_SellGift_TonAmountTitle + #if DEBUG + minAmount = StarsAmount(value: 48000000000, nanos: 0) + #else minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinTonAmount, nanos: 0) + #endif maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxTonAmount, nanos: 0) } case let .paidMessages(_, minAmountValue, _, _, _): @@ -617,7 +621,16 @@ private final class SheetContent: CombinedComponent { return } if state.currency == .stars { - if let amount = state.amount, let tonUsdRate = withdrawConfiguration.tonUsdRate, let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate { + if case .starGiftResell = component.mode, let amount = state.amount, amount.value < 5000 { + #if DEBUG + state.amount = StarsAmount(value: 48000000000, nanos: 0) + #else + state.amount = StarsAmount(value: resaleConfiguration.starGiftResaleMinTonAmount, nanos: 0) + #endif + if let controller = controller() as? StarsWithdrawScreen { + controller.presentMinAmountTooltip(state.amount!.value, currency: .ton) + } + } else if let amount = state.amount, let tonUsdRate = withdrawConfiguration.tonUsdRate, let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate { state.amount = StarsAmount(value: max(min(convertStarsToTon(amount, tonUsdRate: tonUsdRate, starsUsdRate: usdWithdrawRate), resaleConfiguration.starGiftResaleMaxTonAmount), resaleConfiguration.starGiftResaleMinTonAmount), nanos: 0) } else { state.amount = StarsAmount(value: 0, nanos: 0) @@ -1255,6 +1268,8 @@ private final class StarsWithdrawSheetComponent: CombinedComponent { let sheet = Child(SheetComponent<(EnvironmentType)>.self) let animateOut = StoredActionSlot(Action.self) + let sheetExternalState = SheetComponent.ExternalState() + return { context in let environment = context.environment[EnvironmentType.self] @@ -1281,6 +1296,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent { followContentSizeChanges: false, clipsContent: true, isScrollEnabled: false, + externalState: sheetExternalState, animateOut: animateOut ), environment: { @@ -1313,6 +1329,29 @@ private final class StarsWithdrawSheetComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + var sideInset: CGFloat = 0.0 + var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight) + if case .regular = environment.metrics.widthClass { + sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0 + bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight + } + + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + return context.availableSize } } @@ -1357,6 +1396,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { ) self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false } required public init(coder aDecoder: NSCoder) { @@ -1422,10 +1462,8 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { round: false, undoText: nil ), - elevatedLayout: false, - position: .top, action: { _ in return true}) - self.present(resultController, in: .window(.root)) + self.present(resultController, in: .current) if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View { view.animateError() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 54ef6207cb..797719a48d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -114,6 +114,7 @@ swift_library( "//submodules/Components/HierarchyTrackingLayer", "//submodules/Utils/LokiRng", "//submodules/TelegramUI/Components/PeerNameTextComponent", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 58c91e55c7..652d91f344 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -54,6 +54,7 @@ import ChatSendStarsScreen import AnimatedTextComponent import ChatSendAsContextMenu import ShareWithPeersScreen +import AlertComponent private var ObjCKey_DeinitWatcher: Int? @@ -636,17 +637,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let alertController = textAlertController( - context: component.context, - updatedPresentationData: updatedPresentationData, + let alertController = AlertScreen( title: component.strings.Story_AlertStealthModeActiveTitle, text: component.strings.Story_AlertStealthModeActiveText, actions: [ - TextAlertAction(type: .defaultAction, title: component.strings.Common_Cancel, action: {}), - TextAlertAction(type: .genericAction, title: component.strings.Story_AlertStealthModeActiveAction, action: { + .init(title: component.strings.Common_Cancel, type: .default), + .init(title: component.strings.Story_AlertStealthModeActiveAction, type: .generic, action: { action() }) - ] + ], + updatedPresentationData: updatedPresentationData ) alertController.dismissed = { [weak self, weak view] _ in guard let self, let view else { diff --git a/submodules/TelegramUI/Components/TabBarComponent/BUILD b/submodules/TelegramUI/Components/TabBarComponent/BUILD index e08cc9e22b..ad24d1f7d1 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/BUILD +++ b/submodules/TelegramUI/Components/TabBarComponent/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TelegramUI/Components/LiquidLens", "//submodules/AppBundle", "//submodules/SearchBarNode", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index 17ceb59177..51d3786ebe 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -13,62 +13,7 @@ import TextBadgeComponent import LiquidLens import AppBundle import SearchBarNode - -private final class TabSelectionRecognizer: UIGestureRecognizer { - private var initialLocation: CGPoint? - private var currentLocation: CGPoint? - - override init(target: Any?, action: Selector?) { - super.init(target: target, action: action) - - self.delaysTouchesBegan = false - self.delaysTouchesEnded = false - } - - override func reset() { - super.reset() - - self.initialLocation = nil - } - - override func touchesBegan(_ touches: Set, with event: UIEvent) { - super.touchesBegan(touches, with: event) - - if self.initialLocation == nil { - self.initialLocation = touches.first?.location(in: self.view) - } - self.currentLocation = self.initialLocation - - self.state = .began - } - - override func touchesEnded(_ touches: Set, with event: UIEvent) { - super.touchesEnded(touches, with: event) - - self.state = .ended - } - - override func touchesCancelled(_ touches: Set, with event: UIEvent) { - super.touchesCancelled(touches, with: event) - - self.state = .cancelled - } - - override func touchesMoved(_ touches: Set, with event: UIEvent) { - super.touchesMoved(touches, with: event) - - self.currentLocation = touches.first?.location(in: self.view) - - self.state = .changed - } - - func translation(in: UIView?) -> CGPoint { - if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation { - return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y) - } - return CGPoint() - } -} +import TabSelectionRecognizer public final class NavigationSearchView: UIView { private struct Params: Equatable { @@ -774,7 +719,7 @@ public final class TabBarComponent: Component { lensSelection.x = max(0.0, min(lensSelection.x, lensSize.width - lensSelection.width)) - self.liquidLensView.update(size: lensSize, selectionX: lensSelection.x, selectionWidth: lensSelection.width, inset: 4.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition) + self.liquidLensView.update(size: lensSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: lensSize.height), inset: 4.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition) var size = tabsSize diff --git a/submodules/TelegramUI/Components/TabSelectionRecognizer/BUILD b/submodules/TelegramUI/Components/TabSelectionRecognizer/BUILD new file mode 100644 index 0000000000..b3d295294a --- /dev/null +++ b/submodules/TelegramUI/Components/TabSelectionRecognizer/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TabSelectionRecognizer", + module_name = "TabSelectionRecognizer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/TabSelectionRecognizer/Sources/TabSelectionRecognizer.swift b/submodules/TelegramUI/Components/TabSelectionRecognizer/Sources/TabSelectionRecognizer.swift new file mode 100644 index 0000000000..e67f626ed8 --- /dev/null +++ b/submodules/TelegramUI/Components/TabSelectionRecognizer/Sources/TabSelectionRecognizer.swift @@ -0,0 +1,58 @@ +import Foundation +import UIKit + +public final class TabSelectionRecognizer: UIGestureRecognizer { + private var initialLocation: CGPoint? + private var currentLocation: CGPoint? + + public override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.delaysTouchesBegan = false + self.delaysTouchesEnded = false + } + + public override func reset() { + super.reset() + + self.initialLocation = nil + } + + public override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.initialLocation == nil { + self.initialLocation = touches.first?.location(in: self.view) + } + self.currentLocation = self.initialLocation + + self.state = .began + } + + public override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.state = .ended + } + + public override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.state = .cancelled + } + + public override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + self.currentLocation = touches.first?.location(in: self.view) + + self.state = .changed + } + + public func translation(in: UIView?) -> CGPoint { + if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation { + return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y) + } + return CGPoint() + } +} diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 9622b268fe..cb5f59f863 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -106,7 +106,12 @@ public final class TextNodeWithEntities { public let textNode: TextNode private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] - private var enableLooping: Bool = true + public var enableLooping: Bool = true + public var energySavingEnableLooping: Bool = true + + private var effectiveEnableLooping: Bool { + return self.enableLooping && self.energySavingEnableLooping + } public var resetEmojiToFirstFrameAutomatically: Bool = false @@ -124,7 +129,7 @@ public final class TextNodeWithEntities { } else { isItemVisible = false } - let isVisibleForAnimations = self.enableLooping && isItemVisible && itemLayer.enableAnimation + let isVisibleForAnimations = self.effectiveEnableLooping && isItemVisible && itemLayer.enableAnimation if itemLayer.isVisibleForAnimations != isVisibleForAnimations { itemLayer.isVisibleForAnimations = isVisibleForAnimations if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { @@ -257,7 +262,7 @@ public final class TextNodeWithEntities { } private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, attemptSynchronousLoad: Bool, emojiOffset: CGPoint, fontSizeNorm: CGFloat) { - self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji + self.energySavingEnableLooping = context.sharedContext.energyUsageSettings.loopEmoji var nextIndexById: [Int64: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] @@ -292,7 +297,7 @@ public final class TextNodeWithEntities { self.textNode.layer.addSublayer(itemLayer) } itemLayer.enableAnimation = stickerItem.enableAnimation - let isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame) && itemLayer.enableAnimation + let isVisibleForAnimations = self.effectiveEnableLooping && self.isItemVisible(itemRect: itemFrame) && itemLayer.enableAnimation if itemLayer.isVisibleForAnimations != isVisibleForAnimations { if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { itemLayer.reloadAnimation() @@ -410,7 +415,12 @@ public class ImmediateTextNodeWithEntities: TextNode { public var spoilerColor: UIColor = .black public var balancedTextLayout: Bool = false - private var enableLooping: Bool = true + public var enableLooping: Bool = true + public var energySavingEnableLooping: Bool = true + + private var effectiveEnableLooping: Bool { + return self.enableLooping && self.energySavingEnableLooping + } public var arguments: TextNodeWithEntities.Arguments? @@ -423,7 +433,7 @@ public class ImmediateTextNodeWithEntities: TextNode { didSet { if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibility { for (_, itemLayer) in self.inlineStickerItemLayers { - let isVisibleForAnimations = self.enableLooping && self.visibility && itemLayer.enableAnimation + let isVisibleForAnimations = self.effectiveEnableLooping && self.visibility && itemLayer.enableAnimation if itemLayer.isVisibleForAnimations != isVisibleForAnimations { itemLayer.isVisibleForAnimations = isVisibleForAnimations if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { @@ -575,7 +585,7 @@ public class ImmediateTextNodeWithEntities: TextNode { } private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, fontSizeNorm: CGFloat) { - self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji + self.energySavingEnableLooping = context.sharedContext.energyUsageSettings.loopEmoji var nextIndexById: [Int64: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] @@ -613,7 +623,7 @@ public class ImmediateTextNodeWithEntities: TextNode { } itemLayer.enableAnimation = stickerItem.enableAnimation - let isVisibleForAnimations = self.enableLooping && self.visibility && itemLayer.enableAnimation + let isVisibleForAnimations = self.effectiveEnableLooping && self.visibility && itemLayer.enableAnimation if itemLayer.isVisibleForAnimations != isVisibleForAnimations { itemLayer.isVisibleForAnimations = isVisibleForAnimations if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json index d4b2093823..ea33d1b785 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "backspace_24.svg", + "filename" : "backspace_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_24.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_24.svg deleted file mode 100644 index 3907ddf696..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_24.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_30.pdf new file mode 100644 index 0000000000..387f2711d1 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json index a4c1cddbca..55a8ba0a5e 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "keyboard_24.svg", + "filename" : "keyboard_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_24.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_24.svg deleted file mode 100644 index 6134e3e7fa..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_24.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_30.pdf new file mode 100644 index 0000000000..729a574552 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json index 92f837ea2b..f0b8b67785 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "keyboard_2444.svg", + "filename" : "settings_30 (4).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/keyboard_2444.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/keyboard_2444.svg deleted file mode 100644 index 74f7d2fe51..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/keyboard_2444.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/settings_30 (4).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/settings_30 (4).pdf new file mode 100644 index 0000000000..38a6fcde68 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/settings_30 (4).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Contents.json index 5a3751c0b4..94aac7db4a 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Group 1.svg", + "filename" : "trending_44.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Group 1.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Group 1.svg deleted file mode 100644 index d93a88b5d0..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Group 1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/trending_44.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/trending_44.pdf new file mode 100644 index 0000000000..0ce4046658 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/trending_44.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/Contents.json new file mode 100644 index 0000000000..f235c7eaac --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "wear_30 (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/wear_30 (2).pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/wear_30 (2).pdf new file mode 100644 index 0000000000..a6b1772cfd Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/wear_30 (2).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/Contents.json new file mode 100644 index 0000000000..d45ae2798e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "dice.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/dice.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/dice.pdf new file mode 100644 index 0000000000..43f73ca9ae Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/dice.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f76923f6c6..3f6a3fa9dd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2424,6 +2424,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let controller = self.context.sharedContext.makeGiftOfferScreen( context: self.context, + updatedPresentationData: self.updatedPresentationData, gift: gift, peer: peer, amount: amount, @@ -8054,64 +8055,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.presentEmojiList(references: [stickerPackReference], previewIconFile: previewIconFile) } } - - func displayDiceTooltip(dice: TelegramMediaDice) { - guard let _ = dice.value else { - return - } - self.window?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - }) - self.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - return true - }) - let value: String? - let emoji = dice.emoji.strippedEmoji - switch emoji { - case "🎲": - value = self.presentationData.strings.Conversation_Dice_u1F3B2 - case "🎯": - value = self.presentationData.strings.Conversation_Dice_u1F3AF - case "🏀": - value = self.presentationData.strings.Conversation_Dice_u1F3C0 - case "⚽": - value = self.presentationData.strings.Conversation_Dice_u26BD - case "🎰": - value = self.presentationData.strings.Conversation_Dice_u1F3B0 - case "🎳": - value = self.presentationData.strings.Conversation_Dice_u1F3B3 - default: - let emojiHex = emoji.unicodeScalars.map({ String(format:"%02x", $0.value) }).joined().uppercased() - let key = "Conversation.Dice.u\(emojiHex)" - if let string = self.presentationData.strings.primaryComponent.dict[key] { - value = string - } else if let string = self.presentationData.strings.secondaryComponent?.dict[key] { - value = string - } else { - value = nil - } - } - if let value = value { - self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, context: self.context, text: value, action: canSendMessagesToChat(self.presentationInterfaceState) ? self.presentationData.strings.Conversation_SendDice : nil), elevatedLayout: false, action: { [weak self] action in - if let self, canSendMessagesToChat(self.presentationInterfaceState), action == .undo { - self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in - guard let self else { - return - } - self.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), threadId: self.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])], postpone: postpone) - }) - } - return false - }), in: .current) - } - } - func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil, repeatPeriod: Int32? = nil, postpone: Bool = false) -> [EnqueueMessage] { var defaultThreadId: Int64? var defaultReplyMessageSubject: EngineMessageReplySubject? diff --git a/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift b/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift new file mode 100644 index 0000000000..56c6b78a6c --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift @@ -0,0 +1,125 @@ +import Foundation +import AccountContext +import Postbox +import TelegramCore +import SwiftSignalKit +import Display +import TelegramPresentationData +import PresentationDataUtils +import UndoUI +import EmojiGameStakeScreen +import ChatPresentationInterfaceState +import TelegramStringFormatting + +extension ChatControllerImpl { + func presentEmojiGameStake() { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.EmojiGame()) + |> deliverOnMainQueue).start(next: { [weak self] gameInfo in + guard let self, case let .available(info) = gameInfo else { + return + } + let controller = EmojiGameStakeScreen( + context: self.context, + gameInfo: info, + completion: { [weak self] stake in + guard let self else { + return + } + self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let self else { + return + } + self.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: "🎲", tonAmount: stake.value > 0 ? stake.value : nil)), threadId: self.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])], postpone: postpone) + }) + } + ) + self.push(controller) + }) + } + + func displayDiceTooltip(dice: TelegramMediaDice) { + guard let _ = dice.value else { + return + } + self.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + self.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + }) + + let emoji = dice.emoji.strippedEmoji + + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.EmojiGame()) + |> deliverOnMainQueue).start(next: { [weak self] gameInfo in + guard let self else { + return + } + + let canSendMessages = canSendMessagesToChat(self.presentationInterfaceState) + let value: String? + var changeAction: String? + var tonAmount: Int64? + if canSendMessages, emoji == "🎲", case let .available(info) = gameInfo { + //TODO:localize + let currentStake = info.previousStake + value = "Stake: $ \(formatTonAmountText(currentStake, dateTimeFormat: self.presentationData.dateTimeFormat))" + changeAction = "change" + tonAmount = info.previousStake + } else { + switch emoji { + case "🎲": + value = self.presentationData.strings.Conversation_Dice_u1F3B2 + case "🎯": + value = self.presentationData.strings.Conversation_Dice_u1F3AF + case "🏀": + value = self.presentationData.strings.Conversation_Dice_u1F3C0 + case "⚽": + value = self.presentationData.strings.Conversation_Dice_u26BD + case "🎰": + value = self.presentationData.strings.Conversation_Dice_u1F3B0 + case "🎳": + value = self.presentationData.strings.Conversation_Dice_u1F3B3 + default: + let emojiHex = emoji.unicodeScalars.map({ String(format:"%02x", $0.value) }).joined().uppercased() + let key = "Conversation.Dice.u\(emojiHex)" + if let string = self.presentationData.strings.primaryComponent.dict[key] { + value = string + } else if let string = self.presentationData.strings.secondaryComponent?.dict[key] { + value = string + } else { + value = nil + } + } + } + if let value = value { + self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, context: self.context, text: value, action: canSendMessages ? self.presentationData.strings.Conversation_SendDice : nil, changeAction: changeAction), elevatedLayout: false, action: { [weak self] action in + if let self, canSendMessagesToChat(self.presentationInterfaceState) { + switch action { + case .undo: + self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let self else { + return + } + self.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji, tonAmount: tonAmount)), threadId: self.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])], postpone: postpone) + }) + case .info: + if let _ = changeAction { + self.presentEmojiGameStake() + } + default: + break + } + } + return false + }), in: .current) + } + }) + } +} diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index c537e8b80d..b855091607 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -194,7 +194,7 @@ private enum CreateChannelEntry: ItemListNodeEntry { let arguments = arguments as! CreateChannelArguments switch self { case let .channelInfo(_, _, dateTimeFormat, peer, state, avatar): - return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer.flatMap(EnginePeer.init), presence: nil, memberCount: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer.flatMap(EnginePeer.init), presence: nil, memberCount: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, editingNameCompleted: { arguments.focusOnDescription() @@ -202,11 +202,11 @@ private enum CreateChannelEntry: ItemListNodeEntry { arguments.changeProfilePhoto() }, updatingImage: avatar, tag: CreateChannelEntryTag.info) case let .setProfilePhoto(_, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) case let .descriptionSetup(_, text, value): - return ItemListMultilineInputItem(presentationData: presentationData, text: value, placeholder: text, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in + return ItemListMultilineInputItem(presentationData: presentationData, systemStyle: .glass, text: value, placeholder: text, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, tag: CreateChannelEntryTag.description) case let .descriptionInfo(_, text): @@ -214,7 +214,7 @@ private enum CreateChannelEntry: ItemListNodeEntry { case let .usernameHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .username(theme, placeholder, text): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(updatedText) }, action: { }) diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index d68f995c4c..05d3e8a256 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -321,7 +321,7 @@ private enum CreateGroupEntry: ItemListNodeEntry { let arguments = arguments as! CreateGroupArguments switch self { case let .groupInfo(_, _, dateTimeFormat, peer, state, avatar): - return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer.flatMap(EnginePeer.init), presence: nil, memberCount: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer.flatMap(EnginePeer.init), presence: nil, memberCount: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, editingNameCompleted: { arguments.done() @@ -329,13 +329,13 @@ private enum CreateGroupEntry: ItemListNodeEntry { arguments.changeProfilePhoto() }, updatingImage: avatar, tag: CreateGroupEntryTag.info) case let .setProfilePhoto(_, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) case let .usernameHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .username(theme, placeholder, text): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(updatedText) }, action: { }) @@ -364,24 +364,24 @@ private enum CreateGroupEntry: ItemListNodeEntry { case let .usernameInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .topics(_, text): - return ItemListSwitchItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Topics")?.precomposed(), title: text, value: true, enabled: false, sectionId: self.section, style: .blocks, updated: { _ in }) + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Topics")?.precomposed(), title: text, value: true, enabled: false, sectionId: self.section, style: .blocks, updated: { _ in }) case let .topicsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .autoDelete(text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { arguments.updateAutoDelete() }, tag: CreateGroupEntryTag.autoDelete) case let .autoDeleteInfo(text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .member(_, _, _, dateTimeFormat, nameDisplayOrder, peer, presence): - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: presence.flatMap(EnginePeer.Presence.init), text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) + return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: presence.flatMap(EnginePeer.Presence.init), text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) case let .locationHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .location(theme, location): let imageSignal = chatMapSnapshotImage(engine: arguments.context.engine, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) - return ItemListAddressItem(theme: theme, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: nil) + return ItemListAddressItem(theme: theme, systemStyle: .glass, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: nil) case let .changeLocation(_, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeLocation() }) case let .locationInfo(_, text): @@ -389,7 +389,7 @@ private enum CreateGroupEntry: ItemListNodeEntry { case let .venueHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .venue(_, _, venue): - return ItemListVenueItem(presentationData: presentationData, engine: arguments.context.engine, venue: venue, sectionId: self.section, style: .blocks, action: { + return ItemListVenueItem(presentationData: presentationData, systemStyle: .glass, engine: arguments.context.engine, venue: venue, sectionId: self.section, style: .blocks, action: { arguments.updateWithVenue(venue) }) } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 355eacea91..f80fc2a201 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -38,6 +38,7 @@ import TextFormat import BrowserUI import MediaEditorScreen import GiftSetupScreen +import AlertComponent private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -1340,15 +1341,21 @@ func openResolvedUrlImpl( storyProgressPauseContext.update(controller) } } else { - let controller = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Chat_ErrorCantBoost, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - present(controller, nil) - - controller.dismissed = { _ in + let alertController = AlertScreen( + context: context, + title: nil, + text: presentationData.strings.Chat_ErrorCantBoost, + actions: [ + .init(title: presentationData.strings.Common_OK, type: .default) + ] + ) + alertController.dismissed = { _ in dismissedImpl?() } + present(alertController, nil) if let storyProgressPauseContext = contentContext as? StoryProgressPauseContext { - storyProgressPauseContext.update(controller) + storyProgressPauseContext.update(alertController) } } case let .premiumGiftCode(slug): diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 73d0f66575..136c5b944a 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -770,6 +770,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur var startApp: String? var text: String? var profile: Bool = false + var direct: Bool = false var referrer: String? var albumId: Int64? var collectionId: Int64? @@ -823,6 +824,8 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur startChannel = "" } else if queryItem.name == "profile" { profile = true + } else if queryItem.name == "direct" { + direct = true } else if queryItem.name == "startapp" { startApp = "" } @@ -918,6 +921,13 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur convertedUrl = current + "?profile" } } + if direct, let current = convertedUrl { + if current.contains("?") { + convertedUrl = current + "&direct" + } else { + convertedUrl = current + "?direct" + } + } } } else if parsedUrl.host == "hostOverride" { if let components = URLComponents(string: "/?" + query) { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 99ac7849f1..8523d853db 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3317,7 +3317,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { controller.present(alertController, in: .current) dismissAlertImpl = { [weak alertController] in - alertController?.dismissAnimated() + alertController?.dismiss() } } @@ -3865,12 +3865,12 @@ 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 makeGiftOfferScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController { + return giftOfferAlertController(context: context, updatedPresentationData: updatedPresentationData, gift: gift, peer: peer, amount: amount, commit: commit) } - public func makeGiftUpgradeVariantsPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]) -> ViewController { - return GiftUpgradePreviewScreen(context: context, gift: gift, attributes: attributes) + public func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController { + return GiftUpgradeVariantsScreen(context: context, gift: gift, attributes: attributes, selectedAttributes: selectedAttributes, focusedAttribute: focusedAttribute) } public func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController { diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index e408d91083..24ed6920b5 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -18,7 +18,7 @@ public enum UndoOverlayContent { case swipeToReply(title: String, text: String) case actionSucceeded(title: String?, text: String, cancel: String?, destructive: Bool) case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: StickerPackItem?, context: AccountContext) - case dice(dice: TelegramMediaDice, context: AccountContext, text: String, action: String?) + case dice(dice: TelegramMediaDice, context: AccountContext, text: String, action: String?, changeAction: String?) case chatAddedToFolder(context: AccountContext, chatTitle: String, folderTitle: NSAttributedString) case chatRemovedFromFolder(context: AccountContext, chatTitle: String, folderTitle: NSAttributedString) case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 44be88e59a..0360f3cf8f 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -53,6 +53,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private let buttonNode: HighlightTrackingButtonNode private let undoButtonTextNode: ImmediateTextNode private let undoButtonNode: HighlightTrackingButtonNode + + private let changeButtonBackground: ASImageNode + private let changeButtonTextNode: ImmediateTextNode + private let panelNode: ASDisplayNode private let panelWrapperNode: ASDisplayNode private let action: (UndoOverlayAction) -> Bool @@ -103,6 +107,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.buttonNode = HighlightTrackingButtonNode() + self.changeButtonBackground = ASImageNode() + self.changeButtonBackground.displaysAsynchronously = false + self.changeButtonBackground.isUserInteractionEnabled = false + + self.changeButtonTextNode = ImmediateTextNode() + self.changeButtonTextNode.displaysAsynchronously = false + self.changeButtonTextNode.isUserInteractionEnabled = false + var displayUndo = true var undoText = presentationData.strings.Undo_Undo var undoTextColor = presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0) @@ -624,7 +636,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource._asResource(), isVideo: isVideo), width: 80, height: 80, mode: .direct(cachePathPrefix: nil)) } } - case let .dice(dice, context, text, action): + case let .dice(dice, context, text, action, changeAction): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -633,7 +645,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural).mutableCopy() as! NSMutableAttributedString + if let range = attributedText.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: .white) { + attributedText.addAttribute(.attachment, value: icon, range: NSRange(range, in: attributedText.string)) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedText.string)) + } self.textNode.attributedText = attributedText if let action = action { displayUndo = true @@ -651,6 +667,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { default: break } + + if let changeAction { + self.changeButtonTextNode.attributedText = NSAttributedString(string: changeAction, font: Font.regular(12.0), textColor: undoTextColor) + self.changeButtonBackground.image = generateStretchableFilledCircleImage(diameter: 18.0, color: undoTextColor.withMultipliedAlpha(0.1)) + } if dice.emoji == "🎰" { let slotMachineNode = SlotMachineAnimationNode(account: context.account, size: CGSize(width: 42.0, height: 42.0)) @@ -670,8 +691,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { switch stickerPack { case let .result(_, items, _): let item = items[Int(value)] - - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: item.file._parse().resource), width: 120, height: 120, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: item.file._parse().resource), width: 120, height: 120, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) default: break } @@ -1553,7 +1573,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.panelNode.backgroundColor = .clear } self.panelNode.clipsToBounds = true - self.panelNode.cornerRadius = 14.0 + self.panelNode.cornerRadius = 25.0 self.panelWrapperNode = ASDisplayNode() @@ -1625,6 +1645,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.panelWrapperNode.addSubnode(self.undoButtonTextNode) self.panelWrapperNode.addSubnode(self.undoButtonNode) } + if self.changeButtonBackground.image != nil { + self.panelWrapperNode.addSubnode(self.changeButtonBackground) + self.panelWrapperNode.addSubnode(self.changeButtonTextNode) + } self.addSubnode(self.panelNode) self.addSubnode(self.panelWrapperNode) @@ -1962,7 +1986,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } contentHeight += textSize.height - contentHeight = max(49.0, contentHeight) + contentHeight = max(50.0, contentHeight) var insets = layout.insets(options: [.input]) switch self.placementPosition { @@ -2024,6 +2048,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { transition.updateFrame(node: self.textNode, frame: textFrame) } + if self.changeButtonTextNode.supernode != nil { + let changeButtonTextSize = self.changeButtonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let changeButtonFrame = CGRect(origin: CGPoint(x: textFrame.maxX + 10.0, y: floorToScreenPixels(textFrame.midY - changeButtonTextSize.height * 0.5)), size: changeButtonTextSize) + transition.updateFrame(node: self.changeButtonTextNode, frame: changeButtonFrame) + transition.updateFrame(node: self.changeButtonBackground, frame: changeButtonFrame.insetBy(dx: -6.0, dy: -2.0)) + } + if let iconNode = self.iconNode { let iconSize: CGSize if let size = self.iconImageSize { diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index f691900ac3..df20c4e013 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -85,6 +85,7 @@ public enum ParsedInternalPeerUrlParameter { case boost case text(String) case profile + case direct case referrer(String) case storyFolder(Int64) case giftCollection(Int64) @@ -393,6 +394,8 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou return .peer(.name(peerName), .boost) } else if queryItem.name == "profile" { return .peer(.name(peerName), .profile) + } else if queryItem.name == "direct" { + return .peer(.name(peerName), .direct) } else if queryItem.name == "startapp" { var mode: ResolvedStartAppMode = .generic if let queryItems = components.queryItems { @@ -836,6 +839,26 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) switch parameter { case .profile: return .single(.result(.peer(peer._asPeer(), .info(nil)))) + case .direct: + if case let .channel(channel) = peer, let monoforumId = channel.linkedMonoforumId { + return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: monoforumId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return context.engine.peers.findChannelById(channelId: monoforumId.id._internalGetInt64Value()) + } + } + |> map { peer -> ResolveInternalUrlResult in + if let peer { + return .result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) + } else { + return .result(.peer(nil, .info(nil))) + } + } + } else { + return .single(.result(.peer(nil, .info(nil)))) + } case let .text(text): var textInputState: ChatTextInputState? if !text.isEmpty { diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index a812278b61..e0764819f6 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -57,6 +57,7 @@ swift_library( "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 960b3fe902..80a3b10136 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -43,6 +43,7 @@ import GlassBarButtonComponent import BundleIconComponent import LottieComponent import CryptoKit +import AlertComponent private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] @@ -761,12 +762,19 @@ public final class WebAppController: ViewController, AttachmentContainable { func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { var completed = false - let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: message, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { - if !completed { - completed = true - completionHandler() - } - })]) + let alertController = AlertScreen( + context: self.context, + title: nil, + text: message, + actions: [ + .init(title: self.presentationData.strings.Common_OK, action: { + if !completed { + completed = true + completionHandler() + } + }) + ] + ) alertController.dismissed = { byOutsideTap in if byOutsideTap { if !completed { @@ -780,17 +788,25 @@ public final class WebAppController: ViewController, AttachmentContainable { func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { var completed = false - let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: message, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { - if !completed { - completed = true - completionHandler(false) - } - }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { - if !completed { - completed = true - completionHandler(true) - } - })]) + let alertController = AlertScreen( + context: self.context, + title: nil, + text: message, + actions: [ + .init(title: self.presentationData.strings.Common_Cancel, action: { + if !completed { + completed = true + completionHandler(false) + } + }), + .init(title: self.presentationData.strings.Common_OK, type: .default, action: { + if !completed { + completed = true + completionHandler(true) + } + }) + ] + ) alertController.dismissed = { byOutsideTap in if byOutsideTap { if !completed { @@ -1385,7 +1401,7 @@ public final class WebAppController: ViewController, AttachmentContainable { let presentationData = self.presentationData let title = json["title"] as? String - var alertButtons: [TextAlertAction] = [] + var actions: [AlertScreen.Action] = [] for buttonJson in buttons.reversed() { if let button = buttonJson as? [String: Any], let id = button["id"] as? String, let type = button["type"] as? String { @@ -1395,27 +1411,27 @@ public final class WebAppController: ViewController, AttachmentContainable { let text = button["text"] as? String switch type { case "default": - if let text = text { - alertButtons.append(TextAlertAction(type: .genericAction, title: text, action: { + if let text { + actions.append(AlertScreen.Action(title: text, action: { buttonAction() })) } case "destructive": - if let text = text { - alertButtons.append(TextAlertAction(type: .destructiveAction, title: text, action: { + if let text { + actions.append(AlertScreen.Action(title: text, type: .destructive, action: { buttonAction() })) } case "ok": - alertButtons.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + actions.append(AlertScreen.Action(title: presentationData.strings.Common_OK, type: .default, action: { buttonAction() })) case "cancel": - alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + actions.append(AlertScreen.Action(title: presentationData.strings.Common_Cancel, action: { buttonAction() })) case "close": - alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: { + actions.append(AlertScreen.Action(title: presentationData.strings.Common_Close, action: { buttonAction() })) default: @@ -1424,12 +1440,18 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - var actionLayout: TextAlertContentActionLayout = .horizontal - if alertButtons.count > 2 { + var actionLayout: AlertScreen.ActionAligmnent = .default + if actions.count > 2 { actionLayout = .vertical - alertButtons = Array(alertButtons.reversed()) + actions = Array(actions.reversed()) } - let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: title, text: message, actions: alertButtons, actionLayout: actionLayout) + let alertController = AlertScreen( + context: self.context, + configuration: AlertScreen.Configuration(actionAlignment: actionLayout, dismissOnOutsideTap: true, allowInputInset: false), + title: title, + text: message, + actions: actions + ) alertController.dismissed = { byOutsideTap in if byOutsideTap { self.sendAlertButtonEvent(id: nil) @@ -2113,18 +2135,26 @@ public final class WebAppController: ViewController, AttachmentContainable { if result { sendEvent(true) } else { - let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_AllowWriteTitle, text: self.presentationData.strings.WebApp_AllowWriteConfirmation(controller.botName).string, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { - sendEvent(false) - }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in - guard let self else { - return - } - - let _ = (self.context.engine.messages.allowBotSendMessages(botId: controller.botId) - |> deliverOnMainQueue).start(completed: { - sendEvent(true) - }) - })], parseMarkdown: true) + let alertController = AlertScreen( + context: self.context, + title: self.presentationData.strings.WebApp_AllowWriteTitle, + text: self.presentationData.strings.WebApp_AllowWriteConfirmation(controller.botName).string, + actions: [ + .init(title: self.presentationData.strings.Common_Cancel, action: { + sendEvent(false) + }), + .init(title: self.presentationData.strings.Common_OK, type: .default, action: { [weak self] in + guard let self else { + return + } + + let _ = (self.context.engine.messages.allowBotSendMessages(botId: controller.botId) + |> deliverOnMainQueue).start(completed: { + sendEvent(true) + }) + }) + ] + ) alertController.dismissed = { byOutsideTap in if byOutsideTap { sendEvent(false) @@ -2166,48 +2196,56 @@ public final class WebAppController: ViewController, AttachmentContainable { text = self.presentationData.strings.WebApp_SharePhoneConfirmation(botName).string } - let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_SharePhoneTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { - sendEvent(false) - }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in - guard let self, case let .user(user) = accountPeer, let phone = user.phone, !phone.isEmpty else { - return - } - - let sendMessageSignal = enqueueMessages(account: self.context.account, peerId: botId, messages: [ - .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - ]) - |> mapToSignal { messageIds in - if let maybeMessageId = messageIds.first, let messageId = maybeMessageId { - return context.account.pendingMessageManager.pendingMessageStatus(messageId) - |> mapToSignal { status, _ -> Signal in - if status != nil { - return .never() + let alertController = AlertScreen( + context: self.context, + title: self.presentationData.strings.WebApp_SharePhoneTitle, + text: text, + actions: [ + .init(title: self.presentationData.strings.Common_Cancel, action: { + sendEvent(false) + }), + .init(title: self.presentationData.strings.Common_OK, type: .default, action: { [weak self] in + guard let self, case let .user(user) = accountPeer, let phone = user.phone, !phone.isEmpty else { + return + } + + let sendMessageSignal = enqueueMessages(account: self.context.account, peerId: botId, messages: [ + .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + ]) + |> mapToSignal { messageIds in + if let maybeMessageId = messageIds.first, let messageId = maybeMessageId { + return context.account.pendingMessageManager.pendingMessageStatus(messageId) + |> mapToSignal { status, _ -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) } else { - return .single(true) + return .complete() } } - |> take(1) - } else { - return .complete() - } - } - - let sendMessage = { - let _ = (sendMessageSignal - |> deliverOnMainQueue).start(completed: { - sendEvent(true) + + let sendMessage = { + let _ = (sendMessageSignal + |> deliverOnMainQueue).start(completed: { + sendEvent(true) + }) + } + + if requiresUnblock { + let _ = (context.engine.privacy.requestUpdatePeerIsBlocked(peerId: botId, isBlocked: false) + |> deliverOnMainQueue).start(completed: { + sendMessage() + }) + } else { + sendMessage() + } }) - } - - if requiresUnblock { - let _ = (context.engine.privacy.requestUpdatePeerIsBlocked(peerId: botId, isBlocked: false) - |> deliverOnMainQueue).start(completed: { - sendMessage() - }) - } else { - sendMessage() - } - })], parseMarkdown: true) + ] + ) alertController.dismissed = { byOutsideTap in if byOutsideTap { sendEvent(false) diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift index a9090e04e4..9d76690c30 100644 --- a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -23,6 +23,7 @@ import ListItemComponentAdaptor import TelegramStringFormatting import UndoUI import ChatMessagePaymentAlertController +import GlassBarButtonComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -55,7 +56,7 @@ private final class SheetContent: CombinedComponent { } static var body: Body { - let closeButton = Child(Button.self) + let closeButton = Child(GlassBarButtonComponent.self) let title = Child(Text.self) let amountSection = Child(ListSectionComponent.self) let button = Child(ButtonComponent.self) @@ -71,22 +72,31 @@ private final class SheetContent: CombinedComponent { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let sideInset: CGFloat = 16.0 - var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + var contentSize = CGSize(width: context.availableSize.width, height: 36.0) let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)), - 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: 120.0, height: 30.0), + availableSize: CGSize(width: 40.0, height: 40.0), transition: .immediate ) context.add(closeButton - .position(CGPoint(x: closeButton.size.width / 2.0 + sideInset, y: 28.0)) + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) let title = title.update( @@ -95,7 +105,7 @@ private final class SheetContent: CombinedComponent { transition: .immediate ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height)) ) contentSize.height += title.size.height contentSize.height += 40.0 @@ -344,6 +354,7 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(environment.theme.list.blocksBackgroundColor), followContentSizeChanges: false, clipsContent: true,