diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d0f6bdf30b..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"; 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/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 4860336814..16ef6a9ca3 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1005,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) @@ -1519,7 +1518,6 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { let startX = itemView.frame.minX - 4.0 self.selectionGestureState = (startX, startX, itemId) - self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } case .changed: @@ -1538,8 +1536,29 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { guard let index = self.buttons.firstIndex(where: { AnyHashable($0.key) == selectionGestureState.itemId }) else { return } - if self.selectionChanged(self.buttons[index]) { + 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)) @@ -1562,18 +1581,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: @@ -1597,12 +1614,9 @@ 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) @@ -1696,10 +1710,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { strings: self.presentationData.strings, theme: self.presentationData.theme, action: { - }, longPressAction: { [weak self] in - if let strongSelf = self, i == strongSelf.selectedIndex { - strongSelf.longPressed(type) - } + }, + longPressAction: { }) ), environment: {}, @@ -2021,6 +2033,8 @@ 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] { selectionFrame = CGRect(origin: CGPoint(x: itemView.center.x - itemSize.width / 2.0, y: itemView.frame.minY), size: itemSize).insetBy(dx: -3.0, dy: 0.0) @@ -2036,38 +2050,36 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { 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: LiquidLensView + let liquidLensView: LiquidLensView if let current = self.liquidLensView { - backgroundView = current + liquidLensView = current } else { - backgroundView = LiquidLensView(useBackgroundContainer: false) - self.containerNode.view.addSubview(backgroundView) + liquidLensView = LiquidLensView(useBackgroundContainer: false) + self.containerNode.view.addSubview(liquidLensView) //self.containerNode.view.addSubview(self.scrollNode.view) - self.liquidLensView = backgroundView + self.liquidLensView = liquidLensView - backgroundView.contentView.addSubview(self.itemsContainer) - backgroundView.selectedContentView.addSubview(self.selectedItemsContainer) + 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 - backgroundView.addGestureRecognizer(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)) + 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)) - backgroundView.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: 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)) + 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 @@ -2136,8 +2148,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { 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 backgroundView = self.liquidLensView { - containerTransition.updateTransformScale(layer: backgroundView.layer, scale: isAnyButtonVisible ? 0.85 : 1.0) + if let liquidLensView = self.liquidLensView { + containerTransition.updateTransformScale(layer: liquidLensView.layer, scale: isAnyButtonVisible ? 0.85 : 1.0) } if isSelectingUpdated { @@ -2220,8 +2232,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 { @@ -2317,7 +2327,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/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/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 f2bf846b99..0003dd87b9 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/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/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 66a7f5025b..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 @@ -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/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index db1bf254d7..3ea6a6b64d 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -509,6 +509,7 @@ swift_library( "//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 new file mode 100644 index 0000000000..cc21864908 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -0,0 +1,864 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import MultilineTextComponent +import ViewControllerComponent +import ComponentDisplayAdapters +import GlassBackgroundComponent + +public final class AlertComponentEnvironment: Equatable { + public let theme: PresentationTheme + + public init(theme: PresentationTheme) { + self.theme = theme + } + + public static func ==(lhs: AlertComponentEnvironment, rhs: AlertComponentEnvironment) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + return true + } +} + +private final class AlertScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let configuration: AlertScreen.Configuration + let content: [AnyComponentWithIdentity] + let actions: [AlertScreen.Action] + let ready: Promise + + 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() + + private var content: [AnyHashable: ComponentView] = [:] + private var actions: [AnyHashable: ComponentView] = [:] + + private var highlightedAction: AnyHashable? + private let hapticFeedback = HapticFeedback() + + private enum ActionLayout { + case horizontal + case vertical + case verticalReversed + + var isVertical: Bool { + switch self { + case .vertical, .verticalReversed: + return true + default: + return false + } + } + } + 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) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + 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 + } + } + return false + } else { + return super.gestureRecognizerShouldBegin(gestureRecognizer) + } + } + + @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 + } + } + + @objc private func dimTapped() { + guard let component = self.component, component.configuration.dismissOnOutsideTap else { + return + } + self.dismissedByTapOutside = true + self.requestDismiss() + } + + 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) + } + + 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) + } + 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() + } + } + + func updateActionHighlight(previous: Bool) { + guard let component = self.component else { + return + } + 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 + } + } + guard let newHighlightedAction else { + return + } + self.highlightedAction = newHighlightedAction + self.state?.updated(transition: .easeInOut(duration: 0.2)) + } + + 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 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() + } +} 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/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 6ccac87cf8..6a6cdeb1cf 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -988,7 +988,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/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/GiftViewScreen/Sources/TableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift deleted file mode 100644 index d696aef50f..0000000000 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift +++ /dev/null @@ -1,364 +0,0 @@ -import Foundation -import UIKit -import ComponentFlow -import Display -import TelegramPresentationData -import MultilineTextComponent - -final class TableComponent: CombinedComponent { - class Item: Equatable { - enum TitleFont { - case regular - case bold - } - - public let id: AnyHashable - public let title: String? - public let titleFont: TitleFont - public let hasBackground: Bool - 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) { - self.id = AnyHashable(id) - self.title = title - self.titleFont = titleFont - self.hasBackground = hasBackground - 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.titleFont != rhs.titleFont { - return false - } - if lhs.hasBackground != rhs.hasBackground { - 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] - private let semiTransparent: Bool - - public init(theme: PresentationTheme, items: [Item], semiTransparent: Bool = false) { - self.theme = theme - self.items = items - self.semiTransparent = semiTransparent - } - - public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.items != rhs.items { - return false - } - if lhs.semiTransparent != rhs.semiTransparent { - return false - } - return true - } - - final class State: ComponentState { - var cachedLastBackgroundImage: (UIImage, PresentationTheme)? - var cachedLeftColumnImage: (UIImage, PresentationTheme)? - var cachedBorderImage: (UIImage, PresentationTheme)? - } - - func makeState() -> State { - return State() - } - - public static var body: Body { - let leftColumnBackground = Child(Image.self) - let lastBackground = 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 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 - if context.component.semiTransparent { - secondaryBackgroundColor = borderColor.withMultipliedAlpha(0.5) - } - - var leftColumnWidth: CGFloat = 0.0 - - var updatedTitleChildren: [Int: _UpdatedChildComponent] = [:] - var updatedValueChildren: [(_UpdatedChildComponent, UIEdgeInsets)] = [] - var updatedBorderChildren: [_UpdatedChildComponent] = [] - - var i = 0 - for item in context.component.items { - guard let title = item.title else { - i += 1 - continue - } - let titleChild = titleChildren[item.id].update( - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: title, font: item.titleFont == .bold ? Font.semibold(15.0) : Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) - )), - availableSize: context.availableSize, - transition: context.transition - ) - updatedTitleChildren[i] = titleChild - - if titleChild.size.width > leftColumnWidth { - leftColumnWidth = titleChild.size.width - } - i += 1 - } - - leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0) - let rightColumnWidth = context.availableSize.width - leftColumnWidth - - i = 0 - var rowHeights: [Int: CGFloat] = [:] - var totalHeight: CGFloat = 0.0 - var innerTotalHeight: CGFloat = 0.0 - var innerTotalOffset: CGFloat = 0.0 - var hasRowBackground = false - var rowBackgroundIsLast = false - var hasStraightSide = false - - for item in context.component.items { - let insets: UIEdgeInsets - if let customInsets = item.insets { - insets = customInsets - } else { - insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) - } - - var titleHeight: CGFloat = 0.0 - if let titleChild = updatedTitleChildren[i] { - titleHeight = titleChild.size.height - } - - let availableValueWidth: CGFloat - if titleHeight > 0.0 { - availableValueWidth = rightColumnWidth - } else { - availableValueWidth = context.availableSize.width - } - - let valueChild = valueChildren[item.id].update( - component: item.component, - availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height), - transition: context.transition - ) - updatedValueChildren.append((valueChild, insets)) - - let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0) - rowHeights[i] = rowHeight - totalHeight += rowHeight - if titleHeight > 0.0 { - innerTotalHeight += rowHeight - } else if i == 0 { - innerTotalOffset += 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) - } - - if item.hasBackground { - if i != 0 { - rowBackgroundIsLast = true - } - hasRowBackground = true - } - if item.title == nil { - if i != 0 { - rowBackgroundIsLast = true - } - hasStraightSide = true - } - - i += 1 - } - - let borderRadius: CGFloat = 14.0 - - if hasRowBackground { - let lastBackgroundImage: UIImage - if let (currentImage, theme) = context.state.cachedLastBackgroundImage, theme === context.component.theme { - lastBackgroundImage = currentImage - } else { - lastBackgroundImage = 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, height: size.height + borderRadius)) - context.clear(bounds) - - let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0).insetBy(dx: 0.0, dy: rowBackgroundIsLast ? -borderRadius * 2.0 : 0.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.cachedLastBackgroundImage = (lastBackgroundImage, context.component.theme) - } - - let lastRowHeight: CGFloat - let position: CGFloat - if !rowBackgroundIsLast { - lastRowHeight = rowHeights[0] ?? 0 - position = lastRowHeight / 2.0 - } else { - lastRowHeight = rowHeights[i - 1] ?? 0 - position = totalHeight - lastRowHeight / 2.0 - } - let lastBackground = lastBackground.update( - component: Image(image: lastBackgroundImage), - availableSize: CGSize(width: context.availableSize.width, height: lastRowHeight), - transition: context.transition - ) - - context.add( - lastBackground - .position(CGPoint(x: context.availableSize.width / 2.0, y: position)) - ) - } - - 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 - var bounds = CGRect(origin: .zero, size: CGSize(width: size.width + borderRadius, height: size.height)) - context.clear(bounds) - - var offset: CGFloat = 0.0 - if hasStraightSide { - offset = rowBackgroundIsLast ? 0.0 : -borderRadius - - bounds.origin.y += offset - bounds.size.height += borderRadius - } - - 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: innerTotalHeight), - transition: context.transition - ) - context.add(leftColumnBackground - .position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalOffset + innerTotalHeight / 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: innerTotalHeight), - transition: context.transition - ) - context.add( - verticalBorder - .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: innerTotalOffset + innerTotalHeight / 2.0)) - ) - - i = 0 - var originY: CGFloat = 0.0 - for (valueChild, valueInsets) in updatedValueChildren { - let rowHeight = rowHeights[i] ?? 0.0 - - let valueFrame: CGRect - if let titleChild = updatedTitleChildren[i] { - let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) - context.add(titleChild - .position(titleFrame.center) - ) - valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + valueInsets.left, y: originY + verticalPadding), size: valueChild.size) - } else { - if hasRowBackground && rowBackgroundIsLast { - valueFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - valueChild.size.width) / 2.0), y: originY + verticalPadding), size: valueChild.size) - } else { - valueFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: valueChild.size) - } - } - - context.add(valueChild - .position(valueFrame.center) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - - 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)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - } - - originY += rowHeight - i += 1 - } - - return CGSize(width: context.availableSize.width, height: totalHeight) - } - } -} diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index 0940ba237c..d1a721c029 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -1158,6 +1158,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) @@ -1174,7 +1175,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/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/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/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ff94ad36c7..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, 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/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/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,