From 2edce5ebf2c892882be5fa16c9e5570a0c438db9 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 22 Dec 2025 00:19:11 +0400 Subject: [PATCH 1/6] Alerts --- .../Telegram-iOS/en.lproj/Localizable.strings | 7 +- .../Sources/AccountContext.swift | 2 + .../Sources/AttachmentPanel.swift | 2 +- .../BrowserUI/Sources/BrowserWebContent.swift | 28 +- submodules/ChatTextLinkEditUI/BUILD | 19 +- .../Sources/ChatTextLinkEditController.swift | 515 ++------------- .../Source/Components/Image.swift | 12 +- .../FolderInviteLinkListController.swift | 26 +- .../Sources/ChannelAdminController.swift | 27 +- .../ChannelPermissionsController.swift | 36 +- submodules/PremiumUI/BUILD | 1 + .../Sources/GiveawayInfoController.swift | 327 ++-------- .../Sources/AlertTheme.swift | 12 - submodules/PromptUI/BUILD | 17 +- .../PromptUI/Sources/PromptController.swift | 118 ++-- .../Sources/ChangePhoneNumberController.swift | 10 +- .../Sources/MonetizationIntroScreen.swift | 56 +- .../Sources/RevenueWithdrawalController.swift | 50 +- .../Sources/TransactionInfoScreen.swift | 95 ++- submodules/TelegramApi/Sources/Api0.swift | 3 +- submodules/TelegramApi/Sources/Api11.swift | 20 + submodules/TelegramApi/Sources/Api33.swift | 18 +- .../Sources/CallFeedbackController.swift | 6 +- .../Sources/CallRatingController.swift | 492 ++++++-------- .../Sources/CallSuggestTabController.swift | 306 +++------ .../ApiUtils/StoreMessage_Telegram.swift | 8 +- .../PresentationThemeEssentialGraphics.swift | 17 + .../Resources/PresentationResourceKey.swift | 2 + .../Resources/PresentationResourcesChat.swift | 12 + .../Sources/ServiceMessageStrings.swift | 15 +- submodules/TelegramUI/BUILD | 1 + .../AlertComponent/AlertCheckComponent/BUILD | 28 + .../Sources/AlertCheckComponent.swift | 186 ++++++ .../AlertMultilineInputFieldComponent/BUILD | 27 + .../AlertMultilineInputFieldComponent.swift | 346 ++++++++++ .../AlertComponent/AlertTableComponent/BUILD | 23 + .../Sources/AlertTableComponent.swift | 70 ++ .../AlertTransferHeaderComponent/BUILD | 23 + .../AlertTransferHeaderComponent.swift | 126 ++++ .../Components/AlertComponent/BUILD | 2 + .../Sources/AlertActionComponent.swift | 31 +- .../Sources/AlertComponent.swift | 100 +-- .../AlertComponent/Sources/AlertContent.swift | 208 +++++- .../Sources/LegacyAlertComponent.swift | 395 ------------ .../Sources/AttachmentFileController.swift | 7 +- .../Components/AvatarComponent/BUILD | 24 + .../Sources/AvatarComponent.swift | 116 ++++ .../Components/AvatarEditorScreen/BUILD | 1 + .../Sources/PremiumAlertController.swift | 350 +++------- .../ChatAgeRestrictionAlertController/BUILD | 27 + .../ChatAgeRestrictionAlertController.swift | 64 ++ .../ChatMessageAnimatedStickerItemNode.swift | 53 +- .../Sources/ChatMessageBubbleItemNode.swift | 150 ++++- .../ChatMessageDateAndStatusNode.swift | 25 +- .../ChatMessageInstantVideoItemNode.swift | 1 + ...atMessageInteractiveInstantVideoNode.swift | 1 + .../Sources/ChatCallNotificationItem.swift | 14 +- .../Sources/ChatMessageNotificationItem.swift | 18 +- .../ChatMessagePaymentAlertController/BUILD | 2 + .../ChatMessagePaymentAlertController.swift | 513 ++++----------- .../Sources/ChatMessageReplyInfoNode.swift | 11 +- .../Sources/ChatMessageShareButton.swift | 19 +- .../Sources/ChatMessageStickerItemNode.swift | 1 + .../ChatMessageTextBubbleContentNode.swift | 7 +- .../Chat/FactCheckAlertController/BUILD | 3 +- .../Sources/FactCheckAlertController.swift | 448 ++----------- .../Sources/ChatControllerInteraction.swift | 2 + .../Components/ChatThemeScreen/BUILD | 3 + .../Sources/ChatThemeScreen.swift | 10 +- .../GiftThemeTransferAlertController.swift | 322 ++-------- .../Sources/EmojiGameStakeScreen.swift | 62 +- .../Components/Gifts/GiftOptionsScreen/BUILD | 3 + .../GiftAuctionTransferController.swift | 286 +-------- .../Components/Gifts/GiftViewScreen/BUILD | 3 + .../Sources/GiftAuctionBidScreen.swift | 1 - .../Sources/GiftAuctionInfoScreen.swift | 3 +- .../GiftAuctionWearPreviewScreen.swift | 1 + .../Sources/GiftOfferAlertController.swift | 171 +---- .../Sources/GiftPurchaseAlertController.swift | 27 +- .../GiftRemoveInfoAlertController.swift | 390 +++-------- .../Sources/GiftTransferAlertController.swift | 16 +- .../Sources/GiftViewScreen.swift | 72 +-- .../Sources/GiftWithdrawAlertController.swift | 408 +++--------- .../AffiliateProgramSetupScreen/BUILD | 2 + .../Sources/AffiliateProgramSetupScreen.swift | 55 +- .../Sources/TableComponent.swift | 334 ---------- .../Components/PeerInfo/PeerInfoScreen/BUILD | 3 + .../PhotoUpdateConfirmationController.swift | 333 +++------- .../Sources/PeerInfoGiftsPaneNode.swift | 4 +- .../Sources/PeerInfoStoryPaneNode.swift | 4 +- .../OwnershipTransferController/BUILD | 1 + .../ChannelOwnershipTransferController.swift | 82 +-- .../Sources/OwnershipTransferController.swift | 46 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- .../Sources/PasskeysScreen.swift | 23 +- .../Sources/ChannelAppearanceScreen.swift | 44 +- .../Sources/UserApperanceScreen.swift | 46 +- .../Sources/ShareWithPeersScreen.swift | 2 +- .../Stars/StarsWithdrawalScreen/BUILD | 1 + .../StarsRevenueWithdrawalController.swift | 50 +- .../StoryItemSetContainerComponent.swift | 2 +- .../Sources/SuggestedPostApproveAlert.swift | 13 +- .../Components/TextFieldComponent/BUILD | 1 - .../Sources/TextFieldComponent.swift | 32 +- .../CollapseIcon.imageset/Contents.json | 12 + .../Message/CollapseIcon.imageset/summary.pdf | Bin 0 -> 4636 bytes .../Message/ExpandIcon.imageset/Contents.json | 12 + .../Message/ExpandIcon.imageset/original.pdf | Bin 0 -> 4641 bytes .../TelegramUI/Sources/AppDelegate.swift | 38 +- .../Chat/ChatControllerLoadDisplayNode.swift | 36 +- ...ChatControllerNavigationButtonAction.swift | 60 +- ...ChatControllerOpenMessageContextMenu.swift | 13 +- .../ChatControllerOpenMessageFactCheck.swift | 7 +- .../ChatAgeRestrictionAlertController.swift | 302 --------- .../TelegramUI/Sources/ChatController.swift | 110 ++-- .../Sources/ChatControllerAdminBanUsers.swift | 8 +- .../Sources/ChatControllerNode.swift | 5 +- .../ChatControllerOpenAttachmentMenu.swift | 14 +- ...rollerOpenMessageReactionContextMenu.swift | 3 +- .../ChatInterfaceStateContextMenus.swift | 4 +- .../NotificationContainerControllerNode.swift | 10 +- .../NotificationItemContainerNode.swift | 32 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 96 +-- submodules/TelegramUI/Sources/OpenUrl.swift | 4 +- .../Sources/SharedAccountContext.swift | 5 + submodules/WebUI/BUILD | 3 + .../WebAppAddToAttachmentController.swift | 174 +++++ .../Sources/WebAppAlertContentNode.swift | 340 ---------- .../WebUI/Sources/WebAppController.swift | 127 ++-- .../WebAppEmojiStatusAlertController.swift | 561 +++++++--------- .../WebAppLaunchConfirmationController.swift | 607 ++++++------------ .../WebAppLocationAlertController.swift | 307 ++------- .../Sources/WebAppMessagePreviewScreen.swift | 12 +- .../Sources/WebAppSetEmojiStatusScreen.swift | 6 +- .../Sources/WebAppTermsAlertController.swift | 423 ++---------- 135 files changed, 4477 insertions(+), 7394 deletions(-) create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent/BUILD create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent/Sources/AlertCheckComponent.swift create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/BUILD create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertTableComponent/BUILD create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertTableComponent/Sources/AlertTableComponent.swift create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent/BUILD create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent/Sources/AlertTransferHeaderComponent.swift delete mode 100644 submodules/TelegramUI/Components/AlertComponent/Sources/LegacyAlertComponent.swift create mode 100644 submodules/TelegramUI/Components/AvatarComponent/BUILD create mode 100644 submodules/TelegramUI/Components/AvatarComponent/Sources/AvatarComponent.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/Sources/ChatAgeRestrictionAlertController.swift delete mode 100644 submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/TableComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/CollapseIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/CollapseIcon.imageset/summary.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/ExpandIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/ExpandIcon.imageset/original.pdf delete mode 100644 submodules/TelegramUI/Sources/ChatAgeRestrictionAlertController.swift create mode 100644 submodules/WebUI/Sources/WebAppAddToAttachmentController.swift delete mode 100644 submodules/WebUI/Sources/WebAppAlertContentNode.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 574ad64059..8e091cb4ba 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15625,6 +15625,11 @@ Error: %8$@"; "Chat.GiftPurchaseOffer.AcceptConfirmation.BadValue" = "The value of this gift is **%@** higher than the offer."; "Chat.GiftPurchaseOffer.AcceptConfirmation.Confirm" = "Confirm Sale"; - "Notification.PremiumGift.DaysTitle_1" = "%@ Day Premium"; "Notification.PremiumGift.DaysTitle_any" = "%@ Days Premium"; + +"MediaEditor.Audio.Title" = "Audio"; +"MediaEditor.Audio.SavedMusic" = "SAVED MUSIC"; +"MediaEditor.Audio.ShowMore" = "Show More"; + +"WebApp.AddToAttachmentTitle" = "Add to Attachment Menu"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index c792ab7fcb..978c9b3ecb 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1428,6 +1428,8 @@ public protocol SharedAccountContext: AnyObject { func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController + + func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController @available(iOS 13.0, *) func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 5670ec7bc0..ce4e6bfc63 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1144,7 +1144,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { [weak self] link in + let controller = chatTextLinkEditController(context: strongSelf.context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link = link { strongSelf.updateChatPresentationInterfaceState(animated: true, { state in diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index a3d123cf3a..2110704819 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -1320,24 +1320,28 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { var completed = false - let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: prompt, value: defaultText, apply: { value in - if !completed { - completed = true - if let value = value { - completionHandler(value) - } else { - completionHandler(nil) + let promptController = promptController( + context: self.context, + updatedPresentationData: nil, + text: prompt, + value: defaultText, + apply: { value in + if !completed { + completed = true + if let value = value { + completionHandler(value) + } else { + completionHandler(nil) + } } - } - }) - promptController.dismissed = { byOutsideTap in - if byOutsideTap { + }, + dismissed: { if !completed { completed = true completionHandler(nil) } } - } + ) self.present(promptController, nil) } diff --git a/submodules/ChatTextLinkEditUI/BUILD b/submodules/ChatTextLinkEditUI/BUILD index e48e567605..39f5f7832e 100644 --- a/submodules/ChatTextLinkEditUI/BUILD +++ b/submodules/ChatTextLinkEditUI/BUILD @@ -10,14 +10,17 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/Postbox:Postbox", - "//submodules/TelegramCore:TelegramCore", - "//submodules/AccountContext:AccountContext", - "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/UrlEscaping:UrlEscaping", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", + "//submodules/UrlEscaping", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift index c802ca3928..51996c3d55 100644 --- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift +++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift @@ -7,458 +7,81 @@ import TelegramCore import TelegramPresentationData import AccountContext import UrlEscaping +import ComponentFlow +import AlertComponent +import AlertMultilineInputFieldComponent -private final class ChatTextLinkEditInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - fileprivate let textInputNode: EditableTextNode - private let placeholderNode: ASTextNode +public func chatTextLinkEditController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + text: String, + link: String?, + apply: @escaping (String?) -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let inputState = AlertMultilineInputFieldComponent.ExternalState() - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: link != nil ? strings.TextFormat_EditLinkTitle : strings.TextFormat_AddLinkTitle) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.TextFormat_AddLinkText(text).string)) + ) + )) + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertMultilineInputFieldComponent( + context: context, + initialValue: link.flatMap { NSAttributedString(string: $0) }, + placeholder: strings.TextFormat_AddLinkPlaceholder, + keyboardType: .URL, + autocapitalizationType: .none, + autocorrectionType: .no, + isInitiallyFocused: true, + externalState: inputState + ) + ) + )) - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) - - var text: String { - get { - return self.textInputNode.attributedText?.string ?? "" - } - set { - self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) - self.placeholderNode.isHidden = !newValue.isEmpty - } + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - init(theme: PresentationTheme, placeholder: String) { - self.theme = theme - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = EditableTextNode() - self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] - self.textInputNode.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) - self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.keyboardType = .URL - self.textInputNode.autocapitalizationType = .none - self.textInputNode.returnKeyType = .done - self.textInputNode.autocorrectionType = .no - self.textInputNode.tintColor = theme.actionSheet.controlAccentColor - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - super.init() - - self.textInputNode.delegate = self - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.updateTextNodeText(animated: true) - self.textChanged?(editableTextNode.textView.text) - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty - } - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if text == "\n" { - self.complete?() - return false - } - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(33.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.textInputNode.attributedText = nil - self.deactivateInput() - } -} - -private final class ChatTextLinkEditAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let text: String - - private let titleNode: ASTextNode - private let textNode: ASTextNode - let inputFieldNode: ChatTextLinkEditInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - self.inputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - private var isEditing = false - private let allowEmpty: Bool - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, link: String?, allowEmpty: Bool) { - self.strings = strings - self.text = text - self.isEditing = link != nil - self.allowEmpty = allowEmpty - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 2 - - self.inputFieldNode = ChatTextLinkEditInputFieldNode(theme: ptheme, placeholder: strings.TextFormat_AddLinkPlaceholder) - self.inputFieldNode.text = link ?? "" - - 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.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - self.actionNodes.last?.actionEnabled = !(link ?? "").isEmpty - if allowEmpty { - self.actionNodes.last?.actionEnabled = true - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) - } - } - } - - self.inputFieldNode.textChanged = { [weak self] text in - if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { - if strongSelf.allowEmpty { - lastNode.actionEnabled = true + var dismissImpl: (() -> Void)? + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Common_Done, type: .default, action: { + let updatedLink = explicitUrl(inputState.value.string) + if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) { + dismissImpl?() + apply(updatedLink) + } else if inputState.value.string.isEmpty { + dismissImpl?() + apply("") } else { - lastNode.actionEnabled = !text.isEmpty + inputState.animateError() } - } - } - - self.updateTheme(theme) - - if (link ?? "").isEmpty { - Queue.mainQueue().after(0.1, { - let pasteboard = UIPasteboard.general - if pasteboard.hasURLs { - if let url = pasteboard.url?.absoluteString, !url.isEmpty { - self.inputFieldNode.text = url - if let lastNode = self.actionNodes.last { - lastNode.actionEnabled = true - } - self.inputFieldNode.textInputNode.textView.selectAll(nil) - } - } - }) - } - } - - deinit { - self.disposable.dispose() - } - - var link: String { - return self.inputFieldNode.text - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.isEditing ? self.strings.TextFormat_EditLinkTitle : self.strings.TextFormat_AddLinkTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.strings.TextFormat_AddLinkText(self.text).string, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - 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) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 5.0 - - let titleSize = self.titleNode.measure(measureSize) - 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(measureSize) - 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 + 6.0 + spacing - - 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: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + 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 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() + }, autoDismiss: false) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + dismissImpl = { [weak alertController] in + alertController?.dismiss(completion: nil) } -} - -public func chatTextLinkEditController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, account: Account, text: String, link: String?, allowEmpty: Bool = false, apply: @escaping (String?) -> Void) -> AlertController { - let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } - - var dismissImpl: ((Bool) -> Void)? - var applyImpl: (() -> Void)? - - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - apply(nil) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { - applyImpl?() - })] - - let contentNode = ChatTextLinkEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, link: link, allowEmpty: allowEmpty) - contentNode.complete = { - applyImpl?() - } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - let updatedLink = explicitUrl(contentNode.link) - if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) { - dismissImpl?(true) - apply(updatedLink) - } else if allowEmpty && contentNode.link.isEmpty { - dismissImpl?(true) - apply("") - } else { - contentNode.animateError() - } - } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller] animated in - contentNode.inputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + return alertController } diff --git a/submodules/ComponentFlow/Source/Components/Image.swift b/submodules/ComponentFlow/Source/Components/Image.swift index 78475c942f..f18aba2e18 100644 --- a/submodules/ComponentFlow/Source/Components/Image.swift +++ b/submodules/ComponentFlow/Source/Components/Image.swift @@ -6,17 +6,20 @@ public final class Image: Component { public let tintColor: UIColor? public let size: CGSize? public let contentMode: UIImageView.ContentMode + public let cornerRadius: CGFloat public init( image: UIImage?, tintColor: UIColor? = nil, size: CGSize? = nil, - contentMode: UIImageView.ContentMode = .scaleToFill + contentMode: UIImageView.ContentMode = .scaleToFill, + cornerRadius: CGFloat = 0.0 ) { self.image = image self.tintColor = tintColor self.size = size self.contentMode = contentMode + self.cornerRadius = cornerRadius } public static func ==(lhs: Image, rhs: Image) -> Bool { @@ -32,6 +35,9 @@ public final class Image: Component { if lhs.contentMode != rhs.contentMode { return false } + if lhs.cornerRadius != rhs.cornerRadius { + return false + } return true } @@ -47,7 +53,9 @@ public final class Image: Component { func update(component: Image, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { self.image = component.image self.contentMode = component.contentMode - + self.clipsToBounds = component.cornerRadius > 0.0 + + transition.setCornerRadius(layer: self.layer, cornerRadius: component.cornerRadius) transition.setTintColor(view: self, color: component.tintColor ?? .white) switch component.contentMode { diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 2dd9b16a6c..3c39e1cd2b 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -451,7 +451,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese let state = stateValue.with({ $0 }) - let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: presentationData.strings.FolderLinkScreen_NameLink_Title, titleFont: .bold, value: state.title ?? "", characterLimit: 32, apply: { value in + let promptController = promptController(context: context, updatedPresentationData: updatedPresentationData, text: presentationData.strings.FolderLinkScreen_NameLink_Title, titleFont: .bold, value: state.title ?? "", characterLimit: 32, apply: { value in if let value { updateState { state in var state = state @@ -763,15 +763,21 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese if hasChanges { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.FolderLinkScreen_SaveAlertTitle, text: presentationData.strings.FolderLinkScreen_SaveAlertText, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.FolderLinkScreen_SaveAlertActionDiscard, action: { - f() - dismissImpl?() - }), - TextAlertAction(type: .defaultAction, title: state.selectedPeerIds.isEmpty ? presentationData.strings.FolderLinkScreen_SaveAlertActionApply : presentationData.strings.FolderLinkScreen_SaveAlertActionContinue, action: { - applyChangesImpl?() - }) - ]), nil) + let alertController = textAlertController( + context: context, + title: presentationData.strings.FolderLinkScreen_SaveAlertTitle, + text: presentationData.strings.FolderLinkScreen_SaveAlertText, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.FolderLinkScreen_SaveAlertActionDiscard, action: { + f() + dismissImpl?() + }), + TextAlertAction(type: .defaultAction, title: state.selectedPeerIds.isEmpty ? presentationData.strings.FolderLinkScreen_SaveAlertActionApply : presentationData.strings.FolderLinkScreen_SaveAlertActionContinue, action: { + applyChangesImpl?() + }) + ] + ) + presentControllerImpl?(alertController, nil) return false } else { f() diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index deb1d436b9..142f48c511 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -1617,20 +1617,21 @@ public func channelAdminController(context: AccountContext, updatedPresentationD rightNavigationButton = nil footerItem = ChannelAdminAddBotFooterItem(theme: presentationData.theme, title: state.adminRights ? presentationData.strings.Bot_AddToChat_Add_AddAsAdmin : presentationData.strings.Bot_AddToChat_Add_AddAsMember, action: { if state.adminRights { - let theme = AlertControllerTheme(presentationData: presentationData) - let attributedTitle = NSAttributedString(string: presentationData.strings.Bot_AddToChat_Add_AdminAlertTitle, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: theme.primaryColor, paragraphAlignment: .center) - let text = isGroup ? presentationData.strings.Bot_AddToChat_Add_AdminAlertTextGroup(peerTitle).string : presentationData.strings.Bot_AddToChat_Add_AdminAlertTextChannel(peerTitle).string - - let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor) - let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Bot_AddToChat_Add_AdminAlertAdd, action: { - rightButtonActionImpl() - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - })], actionLayout: .vertical) - presentControllerImpl?(controller, nil) + + let alertController = textAlertController( + context: context, + updatedPresentationData: updatedPresentationData, + title: presentationData.strings.Bot_AddToChat_Add_AdminAlertTitle, + text: text, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Bot_AddToChat_Add_AdminAlertAdd, action: { rightButtonActionImpl() + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + ], + actionLayout: .vertical + ) + presentControllerImpl?(alertController, nil) } else { rightButtonActionImpl() } diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index 5e130739d4..701e1c7990 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -1132,22 +1132,26 @@ public func channelPermissionsController(context: AccountContext, updatedPresent controller.navigationPresentation = .modal controller.setState(.custom(icon: .animation("BroadcastGroup"), title: presentationData.strings.BroadcastGroups_IntroTitle, subtitle: nil, text: presentationData.strings.BroadcastGroups_IntroText, buttonTitle: presentationData.strings.BroadcastGroups_Convert, secondaryButtonTitle: presentationData.strings.BroadcastGroups_Cancel, footerText: nil), animated: false) controller.proceed = { [weak controller] result in - let attributedTitle = NSAttributedString(string: presentationData.strings.BroadcastGroups_ConfirmationAlert_Title, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) - let attributedText = parseMarkdownIntoAttributedString(presentationData.strings.BroadcastGroups_ConfirmationAlert_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - let alertController = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Convert, action: { [weak controller] in - controller?.dismiss() - - let _ = (convertGroupToGigagroup(account: context.account, peerId: originalPeerId) - |> deliverOnMainQueue).start(completed: { - let participantsLimit = context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .gigagroupConversion(text: presentationData.strings.BroadcastGroups_Success(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.decimalSeparator)).string), elevatedLayout: true, action: { _ in return false }), nil) - - dismissToChatController?() - }) - })]) + let alertController = textAlertController( + context: context, + updatedPresentationData: updatedPresentationData, + title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Title, + text: presentationData.strings.BroadcastGroups_ConfirmationAlert_Text, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Convert, action: { [weak controller] in + controller?.dismiss() + + let _ = (convertGroupToGigagroup(account: context.account, peerId: originalPeerId) + |> deliverOnMainQueue).start(completed: { + let participantsLimit = context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .gigagroupConversion(text: presentationData.strings.BroadcastGroups_Success(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.decimalSeparator)).string), elevatedLayout: true, action: { _ in return false }), nil) + + dismissToChatController?() + }) + }) + ] + ) controller?.present(alertController, in: .window(.root)) } pushControllerImpl?(controller) diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index c05b9e52c9..f2a16847e2 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -125,6 +125,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/Gifts/TableComponent", "//submodules/TelegramUI/Components/Gifts/PeerTableCellComponent", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/GiveawayInfoController.swift b/submodules/PremiumUI/Sources/GiveawayInfoController.swift index b50ada30e0..2db71bc9b8 100644 --- a/submodules/PremiumUI/Sources/GiveawayInfoController.swift +++ b/submodules/PremiumUI/Sources/GiveawayInfoController.swift @@ -7,8 +7,8 @@ import TelegramCore import AccountContext import TelegramStringFormatting import TelegramPresentationData -import Markdown -import AlertUI +import ComponentFlow +import AlertComponent public func presentGiveawayInfoController( context: AccountContext, @@ -23,6 +23,8 @@ public func presentGiveawayInfoController( peerIds.append(adminId) } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Messages.Message(id: messageId), EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)) @@ -121,22 +123,16 @@ public func presentGiveawayInfoController( channelsCount = 1 + giveawayResults.additionalChannelsCount } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - - let timeZone = TimeZone.current let untilDate = stringForDate(timestamp: untilDateValue, timeZone: timeZone, strings: presentationData.strings) let title: String - let text: String + var text: String var warning: String? - - var dismissImpl: (() -> Void)? - - var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - dismissImpl?() - })] + + var actions: [AlertScreen.Action] = [ + .init(title: presentationData.strings.Common_OK, type: .default) + ] var additionalPrizes = "" if let prizeDescription, !prizeDescription.isEmpty { @@ -322,278 +318,65 @@ public func presentGiveawayInfoController( case .refunded: result = "" warning = presentationData.strings.Chat_Giveaway_Info_Refunded - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Close, action: { - dismissImpl?() - })] case .notWon: result = "**\(presentationData.strings.Chat_Giveaway_Info_DidntWin)**\n\n" case let .wonPremium(slug): result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n" - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: { - dismissImpl?() - openLink(slug) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?() - })] + actions = [ + .init(title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, type: .default, action: { + openLink(slug) + }), + .init(title: presentationData.strings.Common_Cancel) + ] case let .wonStars(stars): let _ = stars result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n" - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: { - dismissImpl?() - openLink("") - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?() - })] + actions = [ + .init(title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, type: .default, action: { + openLink("") + }), + .init(title: presentationData.strings.Common_Cancel) + ] } text = "\(result)\(intro)\(additionalPrizes)\n\n\(ending)" } - let alertController = giveawayInfoAlertController( - context: context, - updatedPresentationData: updatedPresentationData, - title: title, - text: text, - warning: warning, - actions: actions - ) - dismissImpl = { [weak alertController] in - alertController?.dismissAnimated() + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + if let warning { + content.append(AnyComponentWithIdentity( + id: "warning", + component: AnyComponent( + AlertTextComponent(content: .plain(warning), color: .destructive, style: .background(.bold)) + ) + )) } + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) + } + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: actions, + updatedPresentationData: effectiveUpdatedPresentationData + ) present(alertController) }) } - -private final class GiveawayInfoAlertContentNode: AlertContentNode { - private let title: String - private let text: String - private let warning: String? - - private let titleNode: ASTextNode - private let textNode: ASTextNode - fileprivate let warningBackgroundNode: ASImageNode - fileprivate let warningTextNode: ImmediateTextNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - public var theme: PresentationTheme - - public override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - public init(theme: AlertControllerTheme, ptheme: PresentationTheme, title: String, text: String, warning: String?, actions: [TextAlertAction]) { - self.theme = ptheme - self.title = title - self.text = text - self.warning = warning - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 0 - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 0 - - self.warningBackgroundNode = ASImageNode() - self.warningBackgroundNode.displaysAsynchronously = false - - self.warningTextNode = ImmediateTextNode() - self.warningTextNode.maximumNumberOfLines = 0 - self.warningTextNode.lineSpacing = 0.1 - - 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.warningBackgroundNode) - self.addSubnode(self.warningTextNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - } - - public override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor) - let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor) - let attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - self.textNode.attributedText = attributedText - - self.warningTextNode.attributedText = NSAttributedString(string: self.warning ?? "", font: Font.semibold(13.0), textColor: theme.destructiveColor, paragraphAlignment: .center) - self.warningBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: theme.destructiveColor.withAlphaComponent(0.1)) - - 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) - } - } - - public override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - - let titleSize = self.titleNode.measure(measureSize) - 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(measureSize) - 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 + 6.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) - } - } - if "".isEmpty { - effectiveActionLayout = .vertical - } - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - var warningHeight: CGFloat = 0.0 - if let _ = self.warning { - let warningSize = self.warningTextNode.updateLayout(measureSize) - let warningFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - warningSize.width) / 2.0), y: origin.y + 20.0), size: warningSize) - transition.updateFrame(node: self.warningTextNode, frame: warningFrame) - - transition.updateFrame(node: self.warningBackgroundNode, frame: warningFrame.insetBy(dx: -8.0, dy: -8.0)) - - warningHeight += warningSize.height + 26.0 - } - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + 8.0 + actionsHeight + warningHeight + 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 - } -} - -private func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - - let contentNode = GiveawayInfoAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, title: title, text: text, warning: warning, actions: actions) - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - - return controller -} diff --git a/submodules/PresentationDataUtils/Sources/AlertTheme.swift b/submodules/PresentationDataUtils/Sources/AlertTheme.swift index 12c5d1af70..8aceaec6d7 100644 --- a/submodules/PresentationDataUtils/Sources/AlertTheme.swift +++ b/submodules/PresentationDataUtils/Sources/AlertTheme.swift @@ -81,18 +81,6 @@ public func textAlertController( return controller } -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?, diff --git a/submodules/PromptUI/BUILD b/submodules/PromptUI/BUILD index ae10576e23..80d83f292f 100644 --- a/submodules/PromptUI/BUILD +++ b/submodules/PromptUI/BUILD @@ -10,14 +10,17 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/Postbox:Postbox", - "//submodules/TelegramCore:TelegramCore", - "//submodules/AccountContext:AccountContext", - "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", "//submodules/TelegramStringFormatting", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PromptUI/Sources/PromptController.swift b/submodules/PromptUI/Sources/PromptController.swift index 2581f538f3..02ac8f6572 100644 --- a/submodules/PromptUI/Sources/PromptController.swift +++ b/submodules/PromptUI/Sources/PromptController.swift @@ -8,6 +8,9 @@ import TelegramCore import TelegramPresentationData import AccountContext import TelegramStringFormatting +import ComponentFlow +import AlertComponent +import AlertMultilineInputFieldComponent private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { private var theme: PresentationTheme @@ -472,49 +475,90 @@ public enum PromptControllerTitleFont { case bold } -public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, titleFont: PromptControllerTitleFont = .regular, subtitle: String? = nil, value: String?, placeholder: String? = nil, characterLimit: Int = 1000, displayCharacterLimit: Bool = false, apply: @escaping (String?) -> Void) -> AlertController { - let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } +public func promptController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + text: String, + titleFont: PromptControllerTitleFont = .regular, + subtitle: String? = nil, + value: String?, + placeholder: String? = nil, + characterLimit: Int = 1000, + displayCharacterLimit: Bool = false, + apply: @escaping (String?) -> Void, + dismissed: @escaping () -> Void = {} +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings - var dismissImpl: ((Bool) -> Void)? - var applyImpl: (() -> Void)? - - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - apply(nil) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { - dismissImpl?(true) - applyImpl?() - })] - - let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, titleFont: titleFont, subtitle: subtitle, value: value, placeholder: placeholder, characterLimit: characterLimit, displayCharacterLimit: displayCharacterLimit) - contentNode.complete = { - dismissImpl?(true) - applyImpl?() + let inputState = AlertMultilineInputFieldComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + if subtitle == nil && titleFont == .regular { + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + } else { + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: text) + ) + )) } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - apply(contentNode.value) + if let subtitle { + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(subtitle)) + ) + )) + } + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertMultilineInputFieldComponent( + context: context, + initialValue: value.flatMap { NSAttributedString(string: $0) }, + placeholder: placeholder ?? "", + characterLimit: characterLimit, + formatMenuAvailability: .none, + emptyLineHandling: .notAllowed, + isInitiallyFocused: true, + externalState: inputState + ) + ) + )) + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller] animated in - contentNode.inputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel, action: { + apply(nil) + }), + .init(title: strings.Common_Done, type: .default, action: { + apply(inputState.value.string) + }) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + dismissed() } } - return controller + return alertController } private final class AuthAlertContentNode: AlertContentNode { diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift index bda486bba9..d82f5054c0 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift @@ -154,7 +154,15 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll controller?.view.window?.rootViewController?.present(composeController, animated: true, completion: nil) } else { - controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + let alertController = textAlertController( + context: context, + title: nil, + text: presentationData.strings.Login_EmailNotConfiguredError, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ] + ) + controller?.present(alertController, in: .window(.root)) } })) case .generic: diff --git a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift index 2dc7e1687b..b82e565893 100644 --- a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift +++ b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift @@ -12,7 +12,7 @@ import SheetComponent import BundleIconComponent import BalancedTextComponent import MultilineTextComponent -import SolidRoundedButtonComponent +import ButtonComponent import LottieComponent import AccountContext @@ -73,7 +73,7 @@ private final class SheetContent: CombinedComponent { let title = Child(BalancedTextComponent.self) let list = Child(List.self) - let actionButton = Child(SolidRoundedButtonComponent.self) + let actionButton = Child(ButtonComponent.self) let infoBackground = Child(RoundedRectangle.self) let infoTitle = Child(MultilineTextComponent.self) @@ -284,7 +284,7 @@ private final class SheetContent: CombinedComponent { let infoBackground = infoBackground.update( component: RoundedRectangle( color: theme.list.blocksBackgroundColor, - cornerRadius: 10.0 + cornerRadius: 26.0 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: totalInfoHeight), transition: .immediate @@ -307,37 +307,48 @@ private final class SheetContent: CombinedComponent { contentSize.height += infoPadding contentSize.height += spacing + var buttonTitle: [AnyComponentWithIdentity] = [] + buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_ok"), + color: theme.list.itemCheckColors.foregroundColor, + startingPosition: .begin, + size: CGSize(width: 28.0, height: 28.0), + playOnce: state.playOnce + )))) + buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent( + text: strings.Monetization_Intro_Understood, + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )))) + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let actionButton = actionButton.update( - component: SolidRoundedButtonComponent( - title: strings.Monetization_Intro_Understood, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: theme.list.itemCheckColors.fillColor, - backgroundColors: [], - foregroundColor: theme.list.itemCheckColors.foregroundColor + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonTitle, spacing: 2.0)) ), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 10.0, - gloss: false, - iconName: nil, - animationName: nil, - iconPosition: .left, action: { component.dismiss() } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: context.transition ) context.add(actionButton .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) ) contentSize.height += actionButton.size.height - contentSize.height += 22.0 - - contentSize.height += environment.safeInsets.bottom - + contentSize.height += buttonInsets.bottom + state.playAnimationIfNeeded() return contentSize @@ -397,6 +408,7 @@ private final class SheetContainerComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, externalState: sheetExternalState, diff --git a/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift b/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift index 7969836ac8..3a9deb4779 100644 --- a/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift +++ b/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift @@ -8,6 +8,7 @@ import AccountContext import PasswordSetupUI import Markdown import OwnershipTransferController +import AlertComponent func confirmRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -75,38 +76,41 @@ func confirmRevenueWithdrawalController(context: AccountContext, updatedPresenta public func revenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let theme = AlertControllerTheme(presentationData: presentationData) + let strings = presentationData.strings - var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) + var title: String? = strings.OwnershipTransfer_SecurityCheck + var text = strings.Monetization_Withdraw_SecurityRequirements - var text = presentationData.strings.Monetization_Withdraw_SecurityRequirements - let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0 - - var actions: [TextAlertAction] = [] + var actions: [AlertScreen.Action] = [ + .init(title: strings.Common_OK, type: .default) + ] switch initialError { - case .requestPassword: - return confirmRevenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, present: present, completion: completion) - case .twoStepAuthTooFresh, .authSessionTooFresh: - text = text + presentationData.strings.Monetization_Withdraw_ComeBackLater - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] - case .twoStepAuthMissing: - actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.OwnershipTransfer_SetupTwoStepAuth, action: { + case .requestPassword: + return confirmRevenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, present: present, completion: completion) + case .twoStepAuthTooFresh, .authSessionTooFresh: + text = text + strings.Monetization_Withdraw_ComeBackLater + case .twoStepAuthMissing: + actions = [ + .init(title: strings.OwnershipTransfer_SetupTwoStepAuth, type: .default, action: { let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in if shouldDismiss { controller.dismiss() } }) present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})] - default: - title = nil - text = presentationData.strings.Login_UnknownError - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + }), + .init(title: strings.Common_Cancel) + ] + default: + title = nil + text = strings.Login_UnknownError } - let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor) - let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - return richTextAlertController(context: context, title: title, text: attributedText, actions: actions) + return AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + title: title, + text: text, + actions: actions + ) } diff --git a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift index 3a87d208c3..c652c059e8 100644 --- a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift +++ b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift @@ -12,11 +12,11 @@ import SheetComponent import BundleIconComponent import BalancedTextComponent import MultilineTextComponent -import SolidRoundedButtonComponent -import LottieComponent +import ButtonComponent import AccountContext import TelegramStringFormatting import PremiumPeerShortcutComponent +import GlassBarButtonComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -55,18 +55,6 @@ private final class SheetContent: CombinedComponent { } final class State: ComponentState { - var cachedCloseImage: (UIImage, PresentationTheme)? - - let playOnce = ActionSlot() - private var didPlayAnimation = false - - func playAnimationIfNeeded() { - guard !self.didPlayAnimation else { - return - } - self.didPlayAnimation = true - self.playOnce.invoke(Void()) - } } func makeState() -> State { @@ -74,25 +62,23 @@ private final class SheetContent: CombinedComponent { } static var body: Body { - let closeButton = Child(Button.self) + let closeButton = Child(GlassBarButtonComponent.self) let amount = Child(MultilineTextComponent.self) let title = Child(MultilineTextComponent.self) let date = Child(MultilineTextComponent.self) let peerShortcut = Child(PremiumPeerShortcutComponent.self) - let actionButton = Child(SolidRoundedButtonComponent.self) + let actionButton = Child(ButtonComponent.self) return { context in let environment = context.environment[EnvironmentType.self] let component = context.component - let state = context.state let theme = environment.theme let strings = environment.strings let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat - let sideInset: CGFloat = 16.0 + environment.safeInsets.left let textSideInset: CGFloat = 32.0 + environment.safeInsets.left let titleFont = Font.semibold(17.0) @@ -103,26 +89,27 @@ private final class SheetContent: CombinedComponent { var contentSize = CGSize(width: context.availableSize.width, height: 45.0) - let closeImage: UIImage - if let (image, theme) = state.cachedCloseImage, theme === environment.theme { - closeImage = image - } else { - closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! - state.cachedCloseImage = (closeImage, theme) - } - let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Image(image: closeImage)), - action: { [weak component] in - component?.dismiss() + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + component.dismiss() } ), - availableSize: CGSize(width: 30.0, height: 30.0), + availableSize: CGSize(width: 40.0, height: 40.0), transition: .immediate ) context.add(closeButton - .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) let amountString: NSMutableAttributedString @@ -255,27 +242,30 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0)) ) contentSize.height += peerShortcut.size.height - contentSize.height += 50.0 + contentSize.height += 32.0 } else { - contentSize.height += 45.0 + contentSize.height += 27.0 } + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let actionButton = actionButton.update( - component: SolidRoundedButtonComponent( - title: buttonTitle, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: theme.list.itemCheckColors.fillColor, - backgroundColors: [], - foregroundColor: theme.list.itemCheckColors.foregroundColor + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: buttonTitle, + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )) ), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 10.0, - gloss: false, - iconName: nil, - animationName: nil, - iconPosition: .left, action: { component.dismiss() if let explorerUrl { @@ -283,18 +273,14 @@ private final class SheetContent: CombinedComponent { } } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: context.transition ) context.add(actionButton .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) ) contentSize.height += actionButton.size.height - contentSize.height += 22.0 - - contentSize.height += environment.safeInsets.bottom - - state.playAnimationIfNeeded() + contentSize.height += buttonInsets.bottom return contentSize } @@ -360,6 +346,7 @@ private final class SheetContainerComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, externalState: sheetExternalState, diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 7b670a5661..ffaebd35a9 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -433,6 +433,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1548122514] = { return Api.InputNotifyPeer.parse_inputNotifyForumTopic($0) } dict[-1195615476] = { return Api.InputNotifyPeer.parse_inputNotifyPeer($0) } dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($0) } + dict[1528613672] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialFirebasePNV($0) } dict[1009235855] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialPublicKey($0) } dict[-1021329078] = { return Api.InputPasskeyResponse.parse_inputPasskeyResponseLogin($0) } dict[1046713180] = { return Api.InputPasskeyResponse.parse_inputPasskeyResponseRegister($0) } @@ -1421,7 +1422,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1506535550] = { return Api.messages.DiscussionMessage.parse_discussionMessage($0) } dict[1155883043] = { return Api.messages.EmojiGameInfo.parse_emojiGameDiceInfo($0) } dict[1508266805] = { return Api.messages.EmojiGameInfo.parse_emojiGameUnavailable($0) } - dict[-1585592191] = { return Api.messages.EmojiGameOutcome.parse_emojiGameOutcome($0) } + dict[-634726841] = { return Api.messages.EmojiGameOutcome.parse_emojiGameOutcome($0) } dict[-2011186869] = { return Api.messages.EmojiGroups.parse_emojiGroups($0) } dict[1874111879] = { return Api.messages.EmojiGroups.parse_emojiGroupsNotModified($0) } dict[410107472] = { return Api.messages.ExportedChatInvite.parse_exportedChatInvite($0) } diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 4d4f58e93b..671d246b45 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -898,10 +898,17 @@ public extension Api { } public extension Api { enum InputPasskeyCredential: TypeConstructorDescription { + case inputPasskeyCredentialFirebasePNV(pnvToken: String) case inputPasskeyCredentialPublicKey(id: String, rawId: String, response: Api.InputPasskeyResponse) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .inputPasskeyCredentialFirebasePNV(let pnvToken): + if boxed { + buffer.appendInt32(1528613672) + } + serializeString(pnvToken, buffer: buffer, boxed: false) + break case .inputPasskeyCredentialPublicKey(let id, let rawId, let response): if boxed { buffer.appendInt32(1009235855) @@ -915,11 +922,24 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .inputPasskeyCredentialFirebasePNV(let pnvToken): + return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", pnvToken as Any)]) case .inputPasskeyCredentialPublicKey(let id, let rawId, let response): return ("inputPasskeyCredentialPublicKey", [("id", id as Any), ("rawId", rawId as Any), ("response", response as Any)]) } } + public static func parse_inputPasskeyCredentialFirebasePNV(_ reader: BufferReader) -> InputPasskeyCredential? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputPasskeyCredential.inputPasskeyCredentialFirebasePNV(pnvToken: _1!) + } + else { + return nil + } + } public static func parse_inputPasskeyCredentialPublicKey(_ reader: BufferReader) -> InputPasskeyCredential? { var _1: String? _1 = parseString(reader) diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index 253d458276..9803de9170 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1136,15 +1136,16 @@ public extension Api.messages { } public extension Api.messages { enum EmojiGameOutcome: TypeConstructorDescription { - case emojiGameOutcome(seed: Buffer, tonAmount: Int64) + case emojiGameOutcome(seed: Buffer, stakeTonAmount: Int64, tonAmount: Int64) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .emojiGameOutcome(let seed, let tonAmount): + case .emojiGameOutcome(let seed, let stakeTonAmount, let tonAmount): if boxed { - buffer.appendInt32(-1585592191) + buffer.appendInt32(-634726841) } serializeBytes(seed, buffer: buffer, boxed: false) + serializeInt64(stakeTonAmount, buffer: buffer, boxed: false) serializeInt64(tonAmount, buffer: buffer, boxed: false) break } @@ -1152,8 +1153,8 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .emojiGameOutcome(let seed, let tonAmount): - return ("emojiGameOutcome", [("seed", seed as Any), ("tonAmount", tonAmount as Any)]) + case .emojiGameOutcome(let seed, let stakeTonAmount, let tonAmount): + return ("emojiGameOutcome", [("seed", seed as Any), ("stakeTonAmount", stakeTonAmount as Any), ("tonAmount", tonAmount as Any)]) } } @@ -1162,10 +1163,13 @@ public extension Api.messages { _1 = parseBytes(reader) var _2: Int64? _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.EmojiGameOutcome.emojiGameOutcome(seed: _1!, tonAmount: _2!) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.EmojiGameOutcome.emojiGameOutcome(seed: _1!, stakeTonAmount: _2!, tonAmount: _3!) } else { return nil diff --git a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift index 4a47a8b88b..9e8a045458 100644 --- a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift +++ b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift @@ -188,11 +188,11 @@ private enum CallFeedbackControllerEntry: ItemListNodeEntry { case let .reasonsHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .reason(_, reason, title, value): - return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, maximumNumberOfLines: 2, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, maximumNumberOfLines: 2, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleReason(reason, value) }) case let .comment(_, text, placeholder): - return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { updatedText in + return ItemListMultilineInputItem(presentationData: presentationData, systemStyle: .glass, text: text, placeholder: placeholder, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateComment(updatedText) }, updatedFocus: { focused in if focused { @@ -200,7 +200,7 @@ private enum CallFeedbackControllerEntry: ItemListNodeEntry { } }, tag: CallFeedbackControllerEntryTag.comment) case let .includeLogs(_, title, value): - return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleIncludeLogs(value) }) case let .includeLogsInfo(_, text): diff --git a/submodules/TelegramCallsUI/Sources/CallRatingController.swift b/submodules/TelegramCallsUI/Sources/CallRatingController.swift index fa86baa08a..7a08626152 100644 --- a/submodules/TelegramCallsUI/Sources/CallRatingController.swift +++ b/submodules/TelegramCallsUI/Sources/CallRatingController.swift @@ -3,285 +3,13 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display +import ComponentFlow import Postbox import TelegramCore import TelegramPresentationData import TelegramVoip import AccountContext -import AppBundle - -private final class CallRatingAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let apply: (Int) -> Void - - var rating: Int? - - private let titleNode: ASTextNode - private var starContainerNode: ASDisplayNode - private let starNodes: [ASButtonNode] - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], dismiss: @escaping () -> Void, apply: @escaping (Int) -> Void) { - self.strings = strings - self.apply = apply - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 3 - - self.starContainerNode = ASDisplayNode() - - var starNodes: [ASButtonNode] = [] - for _ in 0 ..< 5 { - starNodes.append(ASButtonNode()) - } - self.starNodes = starNodes - - 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.starContainerNode) - - for node in self.starNodes { - node.addTarget(self, action: #selector(self.starPressed(_:)), forControlEvents: .touchDown) - node.addTarget(self, action: #selector(self.starReleased(_:)), forControlEvents: .touchUpInside) - self.starContainerNode.addSubnode(node) - } - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - override func didLoad() { - super.didLoad() - - self.starContainerNode.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) - } - - @objc func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) { - let location = gestureRecognizer.location(in: self.starContainerNode.view) - var selectedNode: ASButtonNode? - for node in self.starNodes { - if node.frame.contains(location) { - selectedNode = node - break - } - } - if let selectedNode = selectedNode { - switch gestureRecognizer.state { - case .began, .changed: - self.starPressed(selectedNode) - case .ended: - self.starReleased(selectedNode) - case .cancelled: - self.resetStars() - default: - break - } - } else { - self.resetStars() - } - } - - private func resetStars() { - for i in 0 ..< self.starNodes.count { - let node = self.starNodes[i] - node.isSelected = false - } - } - - @objc func starPressed(_ sender: ASButtonNode) { - if let index = self.starNodes.firstIndex(of: sender) { - self.rating = index + 1 - for i in 0 ..< self.starNodes.count { - let node = self.starNodes[i] - node.isSelected = i <= index - } - } - } - - @objc func starReleased(_ sender: ASButtonNode) { - if let index = self.starNodes.firstIndex(of: sender) { - self.rating = index + 1 - for i in 0 ..< self.starNodes.count { - let node = self.starNodes[i] - node.isSelected = i <= index - } - if let rating = self.rating { - self.apply(rating) - } - } - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.strings.Calls_RatingTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - for node in self.starNodes { - node.setImage(generateTintedImage(image: UIImage(bundleImageName: "Call/Star"), color: theme.accentColor), for: []) - let highlighted = generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: theme.accentColor) - node.setImage(highlighted, for: [.selected]) - node.setImage(highlighted, for: [.selected, .highlighted]) - } - - 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 - - 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) - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height)) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((resultWidth - titleSize.width) / 2.0), y: origin.y), size: titleSize)) - origin.y += titleSize.height + 13.0 - - let starSize = CGSize(width: 42.0, height: 38.0) - let starsOrigin = floorToScreenPixels((resultWidth - starSize.width * 5.0) / 2.0) - self.starContainerNode.frame = CGRect(origin: CGPoint(x: starsOrigin, y: origin.y), size: CGSize(width: starSize.width * CGFloat(self.starNodes.count), height: starSize.height)) - for i in 0 ..< self.starNodes.count { - let node = self.starNodes[i] - transition.updateFrame(node: node, frame: CGRect(x: starSize.width * CGFloat(i), y: 0.0, width: starSize.width, height: starSize.height)) - } - origin.y += titleSize.height - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + actionsHeight + 56.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 - } -} +import AlertComponent func rateCallAndSendLogs(engine: TelegramEngine, callId: CallId, starsCount: Int, comment: String, userInitiated: Bool, includeLogs: Bool) -> Signal { let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4244000)) @@ -309,35 +37,199 @@ func rateCallAndSendLogs(engine: TelegramEngine, callId: CallId, starsCount: Int } } -public func callRatingController(sharedContext: SharedAccountContext, account: Account, callId: CallId, userInitiated: Bool, isVideo: Bool, present: @escaping (ViewController, Any) -> Void, push: @escaping (ViewController) -> Void) -> AlertController { - let presentationData = sharedContext.currentPresentationData.with { $0 } - let theme = presentationData.theme - let strings = presentationData.strings +public func callRatingController( + sharedContext: SharedAccountContext, + account: Account, + callId: CallId, + userInitiated: Bool, + isVideo: Bool, + present: @escaping (ViewController, Any) -> Void, + push: @escaping (ViewController) -> Void +) -> ViewController { + let strings = sharedContext.currentPresentationData.with { $0 }.strings - var dismissImpl: ((Bool) -> Void)? - var contentNode: CallRatingAlertContentNode? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: { - dismissImpl?(true) - })] + var dismissImpl: (() -> Void)? - contentNode = CallRatingAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, actions: actions, dismiss: { - dismissImpl?(true) - }, apply: { rating in - dismissImpl?(true) - if rating < 4 { - push(callFeedbackController(sharedContext: sharedContext, account: account, callId: callId, rating: rating, userInitiated: userInitiated, isVideo: isVideo)) - } else { - let _ = rateCallAndSendLogs(engine: TelegramEngine(account: account), callId: callId, starsCount: rating, comment: "", userInitiated: userInitiated, includeLogs: false).start() + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent( + title: strings.Calls_RatingTitle, + alignment: .center + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "stars", + component: AnyComponent( + AlertCallRatingComponent(completion: { rating in + dismissImpl?() + if rating < 4 { + push(callFeedbackController(sharedContext: sharedContext, account: account, callId: callId, rating: rating, userInitiated: userInitiated, isVideo: isVideo)) + } else { + let _ = rateCallAndSendLogs(engine: TelegramEngine(account: account), callId: callId, starsCount: rating, comment: "", userInitiated: userInitiated, includeLogs: false).start() + } + }) + ) + )) + + let alertController = AlertScreen( + sharedContext: sharedContext, + content: content, + actions: [ + .init(title: strings.Common_NotNow) + ] + ) + + dismissImpl = { [weak alertController] in + alertController?.dismiss(completion: nil) + } + + return alertController +} + +private final class AlertCallRatingComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + private let completion: (Int) -> Void + + public init(completion: @escaping (Int) -> Void) { + self.completion = completion + } + + public static func ==(lhs: AlertCallRatingComponent, rhs: AlertCallRatingComponent) -> Bool { + return true + } + + public final class View: UIView { + private var containerView: UIView + private let starButtons: [HighlightTrackingButton] + + var rating: Int? + + private var component: AlertCallRatingComponent? + private weak var state: EmptyComponentState? + + public override init(frame: CGRect) { + self.containerView = UIView() + + var starButtons: [HighlightTrackingButton] = [] + for _ in 0 ..< 5 { + starButtons.append(HighlightTrackingButton()) + } + self.starButtons = starButtons + + super.init(frame: frame) + + self.addSubview(self.containerView) + + for button in self.starButtons { + button.addTarget(self, action: #selector(self.starPressed(_:)), for: .touchDown) + button.addTarget(self, action: #selector(self.starReleased(_:)), for: .touchUpInside) + self.containerView.addSubview(button) + } + + self.containerView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) } - }) - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() + + public required init?(coder: NSCoder) { + preconditionFailure() + } + + @objc func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) { + let location = gestureRecognizer.location(in: self.containerView) + var selectedButton: HighlightTrackingButton? + for button in self.starButtons { + if button.frame.contains(location) { + selectedButton = button + break + } + } + if let selectedButton = selectedButton { + switch gestureRecognizer.state { + case .began, .changed: + self.starPressed(selectedButton) + case .ended: + self.starReleased(selectedButton) + case .cancelled: + self.resetStars() + default: + break + } + } else { + self.resetStars() + } + } + + private func resetStars() { + for i in 0 ..< self.starButtons.count { + let node = self.starButtons[i] + node.isSelected = false + } + } + + @objc func starPressed(_ sender: HighlightTrackingButton) { + if let index = self.starButtons.firstIndex(of: sender) { + self.rating = index + 1 + for i in 0 ..< self.starButtons.count { + let node = self.starButtons[i] + node.isSelected = i <= index + } + } + } + + @objc func starReleased(_ sender: HighlightTrackingButton) { + guard let component = self.component else { + return + } + if let index = self.starButtons.firstIndex(of: sender) { + self.rating = index + 1 + for i in 0 ..< self.starButtons.count { + let node = self.starButtons[i] + node.isSelected = i <= index + } + if let rating = self.rating { + component.completion(rating) + } + } + } + + func update(component: AlertCallRatingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + for i in 0 ..< self.starButtons.count { + let button = self.starButtons[i] + button.setImage(UIImage(bundleImageName: "Call/Star")?.withRenderingMode(.alwaysTemplate), for: .normal) + button.setImage(UIImage(bundleImageName: "Call/StarHighlighted")?.withRenderingMode(.alwaysTemplate), for: .selected) + button.setImage(UIImage(bundleImageName: "Call/StarHighlighted")?.withRenderingMode(.alwaysTemplate), for: [.selected, .highlighted]) + } + } + + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let buttonCount = CGFloat(self.starButtons.count) + let starSize = CGSize(width: 42.0, height: 38.0) + let starsOrigin = floorToScreenPixels((availableSize.width - starSize.width * buttonCount) / 2.0) + self.containerView.frame = CGRect(origin: CGPoint(x: starsOrigin, y: 0.0), size: CGSize(width: starSize.width * buttonCount, height: starSize.height)) + for i in 0 ..< self.starButtons.count { + let button = self.starButtons[i] + button.imageView?.tintColor = environment.theme.actionSheet.controlAccentColor + + transition.setFrame(view: button, frame: CGRect(x: starSize.width * CGFloat(i), y: 0.0, width: starSize.width, height: starSize.height)) + } + + return CGSize(width: availableSize.width, height: 38.0) } } - return controller + + 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/TelegramCallsUI/Sources/CallSuggestTabController.swift b/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift index 2d3cfa2218..cfb4c1576b 100644 --- a/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift +++ b/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift @@ -3,237 +3,117 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle +import ComponentFlow +import AlertComponent +import BundleIconComponent -private final class CallSuggestTabAlertContentNode: AlertContentNode { - private let strings: PresentationStrings +public func callSuggestTabController(sharedContext: SharedAccountContext) -> ViewController { + let strings = sharedContext.currentPresentationData.with { $0 }.strings + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertCallSuggestHeaderComponent() + ) + )) + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Calls_CallTabTitle) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Calls_CallTabDescription)) + ) + )) - private let titleNode: ASTextNode - private let textNode: ASTextNode - private let iconNode: ASImageNode - private let accentIconNode: ASImageNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled + let alertController = AlertScreen( + sharedContext: sharedContext, + content: content, + actions: [ + .init(title: strings.Common_NotNow), + .init(title: strings.Calls_AddTab, type: .default, action: { + let _ = updateCallListSettingsInteractively(accountManager: sharedContext.accountManager, { + $0.withUpdatedShowTab(true) + }).start() + }) + ] + ) + return alertController +} + +private final class AlertCallSuggestHeaderComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public init() { } - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) { - self.strings = strings - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 0 - - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - - self.accentIconNode = ASImageNode() - self.accentIconNode.displaysAsynchronously = false - self.accentIconNode.displayWithoutProcessing = true - - 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.iconNode) - self.addSubnode(self.accentIconNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) + public static func ==(lhs: AlertCallSuggestHeaderComponent, rhs: AlertCallSuggestHeaderComponent) -> Bool { + return true } - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: strings.Calls_CallTabTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: strings.Calls_CallTabDescription, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/AlertIcon"), color: theme.controlBorderColor) - self.accentIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/AlertAccentIcon"), color: theme.accentColor) + public final class View: UIView { + private let image = ComponentView() + private let accentImage = ComponentView() - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } + private var component: AlertCallSuggestHeaderComponent? + private weak var state: EmptyComponentState? - 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 titleSize = self.titleNode.measure(size) - 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 + 13.0 - - var iconSize = CGSize() - if let icon = self.iconNode.image { - iconSize = icon.size - let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: origin.y), size: iconSize) - transition.updateFrame(node: self.iconNode, frame: iconFrame) - transition.updateFrame(node: self.accentIconNode, frame: iconFrame) - origin.y += iconSize.height + 16.0 - } - - let textSize = self.textNode.measure(size) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) - - 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) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - let resultSize = CGSize(width: resultWidth, height: titleSize.height + iconSize.height + textSize.height + actionsHeight + 34.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))) + func update(component: AlertCallSuggestHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let imageSize = self.image.update( + transition: .immediate, + component: AnyComponent( + BundleIconComponent(name: "Call List/AlertIcon", tintColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.2)) + ), + environment: {}, + containerSize: availableSize + ) + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: 0.0), size: imageSize) + if let imageView = self.image.view { + if imageView.superview == nil { + self.addSubview(imageView) } - } - 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 + imageView.frame = imageFrame } - 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 + let _ = self.accentImage.update( + transition: .immediate, + component: AnyComponent( + BundleIconComponent(name: "Call List/AlertAccentIcon", tintColor: environment.theme.actionSheet.controlAccentColor) + ), + environment: {}, + containerSize: availableSize + ) + let accentImageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: 0.0), size: imageSize) + if let accentImageView = self.accentImage.view { + if accentImageView.superview == nil { + self.addSubview(accentImageView) + } + accentImageView.frame = accentImageFrame } - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - return resultSize - } -} - -func callSuggestTabController(sharedContext: SharedAccountContext) -> AlertController { - let presentationData = sharedContext.currentPresentationData.with { $0 } - let theme = presentationData.theme - let strings = presentationData.strings - - var dismissImpl: ((Bool) -> Void)? - var contentNode: CallSuggestTabAlertContentNode? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Calls_AddTab, action: { - dismissImpl?(true) - let _ = updateCallListSettingsInteractively(accountManager: sharedContext.accountManager, { - $0.withUpdatedShowTab(true) - }).start() - })] - - contentNode = CallSuggestTabAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, actions: actions) - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() + return CGSize(width: availableSize.width, height: imageSize.height) } } - return controller + + 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/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index bdf7ff56a4..9b2457ab72 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -459,13 +459,15 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } case let .messageMediaDice(_, value, emoticon, apiGameOutcome): var gameOutcome: TelegramMediaDice.GameOutcome? + var tonAmount: Int64? switch apiGameOutcome { - case let .emojiGameOutcome(seed, tonAmount): - gameOutcome = TelegramMediaDice.GameOutcome(seed: seed.makeData(), tonAmount: tonAmount) + case let .emojiGameOutcome(seed, stakeTonAmount, outcomeTonAmount): + gameOutcome = TelegramMediaDice.GameOutcome(seed: seed.makeData(), tonAmount: outcomeTonAmount) + tonAmount = stakeTonAmount default: break } - return (TelegramMediaDice(emoji: emoticon, value: value, gameOutcome: gameOutcome), nil, nil, nil, nil, nil) + return (TelegramMediaDice(emoji: emoticon, tonAmount: tonAmount, value: value, gameOutcome: gameOutcome), nil, nil, nil, nil, nil) case let .messageMediaStory(flags, peerId, id, _): let isMention = (flags & (1 << 1)) != 0 return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil, nil) diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index a2c2a1dabb..4d80c19101 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -175,6 +175,11 @@ public final class PrincipalThemeEssentialGraphics { public let outgoingDateAndStatusStarsIcon: UIImage public let mediaStarsIcon: UIImage public let freeStarsIcon: UIImage + + public let incomingDateAndStatusTonIcon: UIImage + public let outgoingDateAndStatusTonIcon: UIImage + public let mediaTonIcon: UIImage + public let freeTonIcon: UIImage public let incomingDateAndStatusPinnedIcon: UIImage public let outgoingDateAndStatusPinnedIcon: UIImage @@ -369,6 +374,12 @@ public final class PrincipalThemeEssentialGraphics { self.outgoingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.outgoing.secondaryTextColor)! self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)! self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)! + + let tonImage = generateScaledImage(image: UIImage(bundleImageName: "Ads/TonMedium"), size: CGSize(width: 12.0, height: 12.0), opaque: false)! + self.incomingDateAndStatusTonIcon = generateTintedImage(image: tonImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusTonIcon = generateTintedImage(image: tonImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaTonIcon = generateTintedImage(image: tonImage, color: .white)! + self.freeTonIcon = generateTintedImage(image: tonImage, color: serviceColor.primaryText)! let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! @@ -497,6 +508,12 @@ public final class PrincipalThemeEssentialGraphics { self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)! self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)! + let tonImage = generateScaledImage(image: UIImage(bundleImageName: "Ads/TonMedium"), size: CGSize(width: 12.0, height: 12.0), opaque: false)! + self.incomingDateAndStatusTonIcon = generateTintedImage(image: tonImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusTonIcon = generateTintedImage(image: tonImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaTonIcon = generateTintedImage(image: tonImage, color: .white)! + self.freeTonIcon = generateTintedImage(image: tonImage, color: serviceColor.primaryText)! + let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)! diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 504d20f5dc..bdb4dec820 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -308,6 +308,8 @@ public enum PresentationResourceKey: Int32 { case chatFreeShareButtonIcon case chatFreeCloseButtonIcon case chatFreeMoreButtonIcon + case chatFreeExpandButtonIcon + case chatFreeCollapseButtonIcon case chatKeyboardActionButtonMessageIcon case chatKeyboardActionButtonLinkIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index d2e5ce37d7..ccb452e605 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1174,6 +1174,18 @@ public struct PresentationResourcesChat { }) } + public static func chatFreeExpandButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? { + return theme.image(PresentationResourceKey.chatFreeExpandButtonIcon.rawValue, { _ in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ExpandIcon"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper)) + }) + } + + public static func chatFreeCollapseButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? { + return theme.image(PresentationResourceKey.chatFreeCollapseButtonIcon.rawValue, { _ in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CollapseIcon"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper)) + }) + } + public static func chatKeyboardActionButtonMessageIconImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatKeyboardActionButtonMessageIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: theme.chat.inputButtonPanel.buttonTextColor) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 5a0c2ea7fb..9d17cf1989 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1765,10 +1765,19 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } else if let dice = media as? TelegramMediaDice, let gameOutcome = dice.gameOutcome { - if let value = dice.value, value > 1 { + if let value = dice.value { //TODO:localize - let value = formatTonAmountText(gameOutcome.tonAmount, dateTimeFormat: dateTimeFormat) - let attributedText = NSMutableAttributedString(string: "You won $\(value)", font: titleFont, textColor: primaryTextColor) + + let rawString: String + if value == 1, let tonAmount = dice.tonAmount { + let value = formatTonAmountText(tonAmount, dateTimeFormat: dateTimeFormat) + rawString = "You lost $\(value)" + } else { + let value = formatTonAmountText(gameOutcome.tonAmount, dateTimeFormat: dateTimeFormat) + rawString = "You won $\(value)" + } + + let attributedText = NSMutableAttributedString(string: rawString, font: titleFont, textColor: primaryTextColor) if let range = attributedText.string.range(of: "$") { attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: true)), range: NSRange(range, in: attributedText.string)) attributedText.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedText.string)) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 3ea6a6b64d..083f6d1a3b 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -510,6 +510,7 @@ swift_library( "//submodules/TelegramUI/Components/Gifts/GiftDemoScreen", "//submodules/TelegramUI/Components/EmojiGameStakeScreen", "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent/BUILD b/submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent/BUILD new file mode 100644 index 0000000000..d6049dc518 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AlertCheckComponent", + module_name = "AlertCheckComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TextFormat", + "//submodules/Markdown", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/CheckComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent/Sources/AlertCheckComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent/Sources/AlertCheckComponent.swift new file mode 100644 index 0000000000..a9c1d04b19 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent/Sources/AlertCheckComponent.swift @@ -0,0 +1,186 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramPresentationData +import AlertComponent +import PlainButtonComponent +import MultilineTextComponent +import CheckComponent +import TextFormat +import Markdown + +public final class AlertCheckComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public class ExternalState { + public fileprivate(set) var value: Bool + fileprivate var valuePromise = Promise() + public var valueSignal: Signal + + public init() { + self.value = false + self.valueSignal = self.valuePromise.get() + } + } + + let title: String + let initialValue: Bool + let externalState: ExternalState + let linkAction: (() -> Void)? + + public init( + title: String, + initialValue: Bool, + externalState: ExternalState, + linkAction: (() -> Void)? = nil + ) { + self.title = title + self.initialValue = initialValue + self.externalState = externalState + self.linkAction = linkAction + } + + public static func ==(lhs: AlertCheckComponent, rhs: AlertCheckComponent) -> Bool { + return true + } + + public final class View: UIView { + private let button = ComponentView() + + private var component: AlertCheckComponent? + private weak var state: EmptyComponentState? + + private var isUpdating = false + + private var valuePromise = ValuePromise(false) + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + func findTextView(view: UIView?) -> ImmediateTextView? { + if let view { + if let view = view as? ImmediateTextView { + return view + } + for view in view.subviews { + if let result = findTextView(view: view) { + return result + } + } + } + return nil + } + let result = super.hitTest(point, with: event) + if let textView = findTextView(view: result) { + if let (_, attributes) = textView.attributesAtPoint(self.convert(point, to: textView)) { + if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil { + return textView + } + } + } + return result + } + + func update(component: AlertCheckComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + if self.component == nil { + component.externalState.value = component.initialValue + component.externalState.valuePromise.set(self.valuePromise.get()) + } + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let checkTheme = CheckComponent.Theme( + backgroundColor: environment.theme.list.itemCheckColors.fillColor, + strokeColor: environment.theme.list.itemCheckColors.foregroundColor, + borderColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.15), + overlayBorder: false, + hasInset: false, + hasShadow: false + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = environment.theme.actionSheet.primaryTextColor + let linkColor = environment.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 buttonSize = self.button.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent( + theme: checkTheme, + size: CGSize(width: 18.0, height: 18.0), + selected: component.externalState.value + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .markdown(text: component.title, attributes: markdownAttributes), + maximumNumberOfLines: 2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + component.linkAction?() + } + } + ))) + ], spacing: 10.0)), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.externalState.value = !component.externalState.value + self.valuePromise.set(component.externalState.value) + + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }, + animateAlpha: false, + animateScale: false + )), + environment: { + }, + containerSize: CGSize(width: availableSize.width + 20.0, height: 1000.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: 7.0), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + + return CGSize(width: availableSize.width, height: buttonSize.height + 7.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/AlertComponent/AlertMultilineInputFieldComponent/BUILD b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/BUILD new file mode 100644 index 0000000000..8a8f24ed70 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AlertMultilineInputFieldComponent", + module_name = "AlertMultilineInputFieldComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/TextFieldComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift new file mode 100644 index 0000000000..93e6b133d9 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift @@ -0,0 +1,346 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AlertComponent +import TextFieldComponent +import MultilineTextComponent +import AccountContext +import TextFormat + +public final class AlertMultilineInputFieldComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public class ExternalState { + public fileprivate(set) var value: NSAttributedString = NSAttributedString() + public fileprivate(set) var animateError: () -> Void = {} + fileprivate let valuePromise = ValuePromise(NSAttributedString()) + public var valueSignal: Signal { + return self.valuePromise.get() + } + + public var textAndEntities: (String, [MessageTextEntity]) { + let text = self.value.string + let entities = generateChatInputTextEntities(self.value) + return (text, entities) + } + + public init() { + } + } + + public enum FormatMenuAvailability: Equatable { + public enum Action: CaseIterable { + case bold + case italic + case monospace + case link + case strikethrough + case underline + case spoiler + case quote + case code + + public static var all: [Action] = [ + .bold, + .italic, + .monospace, + .link, + .strikethrough, + .underline, + .spoiler, + .quote, + .code + ] + + var textFieldValue: TextFieldComponent.FormatMenuAvailability.Action { + switch self { + case .bold: + return .bold + case .italic: + return .italic + case .monospace: + return .monospace + case .link: + return .link + case .strikethrough: + return .strikethrough + case .underline: + return .underline + case .spoiler: + return .spoiler + case .quote: + return .quote + case .code: + return .code + } + } + } + case available([Action]) + case none + + var textFieldValue: TextFieldComponent.FormatMenuAvailability { + switch self { + case let .available(actions): + return .available(actions.map { $0.textFieldValue }) + case .none: + return .none + } + } + } + + public enum EmptyLineHandling { + case allowed + case oneConsecutive + case notAllowed + + var textFieldValue: TextFieldComponent.EmptyLineHandling { + switch self { + case .allowed: + return .allowed + case .oneConsecutive: + return .oneConsecutive + case .notAllowed: + return .notAllowed + } + } + } + + let context: AccountContext + let initialValue: NSAttributedString? + let placeholder: String + let prefix: NSAttributedString? + let characterLimit: Int? + let keyboardType: UIKeyboardType + let autocapitalizationType: UITextAutocapitalizationType + let autocorrectionType: UITextAutocorrectionType + let formatMenuAvailability: FormatMenuAvailability + let emptyLineHandling: EmptyLineHandling + let isInitiallyFocused: Bool + let externalState: ExternalState + let present: (ViewController) -> Void + let returnKeyAction: (() -> Void)? + + public init( + context: AccountContext, + initialValue: NSAttributedString? = nil, + placeholder: String, + prefix: NSAttributedString? = nil, + characterLimit: Int? = nil, + keyboardType: UIKeyboardType = .default, + autocapitalizationType: UITextAutocapitalizationType = .sentences, + autocorrectionType: UITextAutocorrectionType = .default, + formatMenuAvailability: FormatMenuAvailability = .none, + emptyLineHandling: EmptyLineHandling = .allowed, + isInitiallyFocused: Bool = false, + externalState: ExternalState, + present: @escaping (ViewController) -> Void = { _ in }, + returnKeyAction: (() -> Void)? = nil + ) { + self.context = context + self.initialValue = initialValue + self.placeholder = placeholder + self.prefix = prefix + self.characterLimit = characterLimit + self.keyboardType = keyboardType + self.autocapitalizationType = autocapitalizationType + self.autocorrectionType = autocorrectionType + self.formatMenuAvailability = formatMenuAvailability + self.emptyLineHandling = emptyLineHandling + self.isInitiallyFocused = isInitiallyFocused + self.externalState = externalState + self.present = present + self.returnKeyAction = returnKeyAction + } + + public static func ==(lhs: AlertMultilineInputFieldComponent, rhs: AlertMultilineInputFieldComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.initialValue != rhs.initialValue { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.prefix != rhs.prefix { + return false + } + if lhs.characterLimit != rhs.characterLimit { + return false + } + if lhs.keyboardType != rhs.keyboardType { + return false + } + if lhs.autocapitalizationType != rhs.autocapitalizationType { + return false + } + if lhs.autocorrectionType != rhs.autocorrectionType { + return false + } + if lhs.formatMenuAvailability != rhs.formatMenuAvailability { + return false + } + if lhs.emptyLineHandling != rhs.emptyLineHandling { + return false + } + if lhs.isInitiallyFocused != rhs.isInitiallyFocused { + return false + } + return true + } + + public final class View: UIView { + private let background = ComponentView() + private let textField = ComponentView() + private let textFieldExternalState = TextFieldComponent.ExternalState() + private let placeholder = ComponentView() + + private var component: AlertMultilineInputFieldComponent? + private weak var state: EmptyComponentState? + + func animateError() { + if let textFieldView = self.textField.view { + textFieldView.layer.addShakeAnimation() + } + HapticFeedback().error() + } + + func update(component: AlertMultilineInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + var resetText: NSAttributedString? + if self.component == nil { + resetText = component.initialValue + component.externalState.animateError = { [weak self] in + self?.animateError() + } + } + + let isFirstTime = self.component == nil + + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let topInset: CGFloat = 15.0 + let horizontalInset: CGFloat = 4.0 + + let textFieldSize = self.textField.update( + transition: transition, + component: AnyComponent(TextFieldComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + externalState: self.textFieldExternalState, + fontSize: 17.0, + textColor: environment.theme.actionSheet.primaryTextColor, + accentColor: environment.theme.actionSheet.controlAccentColor, + insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), + hideKeyboard: false, + customInputView: nil, + resetText: resetText, + isOneLineWhenUnfocused: false, + characterLimit: component.characterLimit, + emptyLineHandling: component.emptyLineHandling.textFieldValue, + formatMenuAvailability: component.formatMenuAvailability.textFieldValue, + returnKeyType: .done, + keyboardType: component.keyboardType, + autocapitalizationType: component.autocapitalizationType, + autocorrectionType: component.autocorrectionType, + lockedFormatAction: { + }, + present: { [weak self] c in + guard let self, let component = self.component else { + return + } + component.present(c) + }, + paste: { _ in + }, + returnKeyAction: { [weak self] in + guard let self, let component = self.component else { + return + } + component.returnKeyAction?() + }, + backspaceKeyAction: { + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width + horizontalInset * 2.0, height: availableSize.height) + ) + component.externalState.value = self.textFieldExternalState.text + component.externalState.valuePromise.set(component.externalState.value) + + let backgroundPadding: CGFloat = 14.0 + let size = CGSize(width: availableSize.width, height: max(50.0, textFieldSize.height + 22.0)) + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent( + FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false) + ), + environment: {}, + containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height) + ) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let textFieldFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textFieldSize.width) / 2.0), y: topInset + 11.0 - UIScreenPixel), size: textFieldSize) + if let textFieldView = self.textField.view { + if textFieldView.superview == nil { + self.addSubview(textFieldView) + self.textField.parentState = state + } + transition.setFrame(view: textFieldView, frame: textFieldFrame) + } + + let placeholderSize = self.placeholder.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString( + string: component.placeholder, + font: Font.regular(17.0), + textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4) + ))) + ), + environment: {}, + containerSize: CGSize(width: size.width, height: 50.0) + ) + let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize) + if let placeholderView = self.placeholder.view { + if placeholderView.superview == nil { + placeholderView.isUserInteractionEnabled = false + self.addSubview(placeholderView) + } + placeholderView.frame = placeholderFrame + placeholderView.isHidden = self.textFieldExternalState.hasText + } + + if isFirstTime && component.isInitiallyFocused { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.activateInput() + } + } + + return CGSize(width: availableSize.width, height: size.height + topInset) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertTableComponent/BUILD b/submodules/TelegramUI/Components/AlertComponent/AlertTableComponent/BUILD new file mode 100644 index 0000000000..9c1d5ce5f6 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertTableComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AlertTableComponent", + module_name = "AlertTableComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/Gifts/TableComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertTableComponent/Sources/AlertTableComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertTableComponent/Sources/AlertTableComponent.swift new file mode 100644 index 0000000000..aabc6e85b9 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertTableComponent/Sources/AlertTableComponent.swift @@ -0,0 +1,70 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import TelegramPresentationData +import AlertComponent +import TableComponent + +public final class AlertTableComponent: Component { + public 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/AlertComponent/AlertTransferHeaderComponent/BUILD b/submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent/BUILD new file mode 100644 index 0000000000..c6734ce781 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AlertTransferHeaderComponent", + module_name = "AlertTransferHeaderComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AlertComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent/Sources/AlertTransferHeaderComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent/Sources/AlertTransferHeaderComponent.swift new file mode 100644 index 0000000000..b0ddb6e28c --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent/Sources/AlertTransferHeaderComponent.swift @@ -0,0 +1,126 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import TelegramPresentationData +import AlertComponent +import BundleIconComponent + +public final class AlertTransferHeaderComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public enum IconType { + case transfer + case take + } + + let fromComponent: AnyComponentWithIdentity + let toComponent: AnyComponentWithIdentity + let type: IconType + + public init( + fromComponent: AnyComponentWithIdentity, + toComponent: AnyComponentWithIdentity, + type: IconType + ) { + self.fromComponent = fromComponent + self.toComponent = toComponent + self.type = type + } + + public static func ==(lhs: AlertTransferHeaderComponent, rhs: AlertTransferHeaderComponent) -> Bool { + if lhs.fromComponent != rhs.fromComponent { + return false + } + if lhs.toComponent != rhs.toComponent { + return false + } + if lhs.type != rhs.type { + 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 + let iconName: String + switch component.type { + case .transfer: + iconName = "Peer Info/AlertArrow" + size = CGSize(width: 148.0, height: 60.0) + case .take: + iconName = "Media Editor/CutoutUndo" + size = CGSize(width: 154.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: iconName, 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) + } +} diff --git a/submodules/TelegramUI/Components/AlertComponent/BUILD b/submodules/TelegramUI/Components/AlertComponent/BUILD index 0fc1a866f5..f84c8e133c 100644 --- a/submodules/TelegramUI/Components/AlertComponent/BUILD +++ b/submodules/TelegramUI/Components/AlertComponent/BUILD @@ -15,12 +15,14 @@ swift_library( "//submodules/ComponentFlow", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", + "//submodules/TelegramCore", "//submodules/AccountContext", "//submodules/Markdown", "//submodules/TextFormat", "//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/ViewControllerComponent", "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/Components/ActivityIndicatorComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift index dd32935321..4f330f6bcc 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift @@ -33,17 +33,20 @@ final class AlertActionComponent: Component { let theme: Theme let title: String let isHighlighted: Bool + let isEnabled: Signal let progress: Signal init( theme: Theme, title: String, isHighlighted: Bool, + isEnabled: Signal, progress: Signal ) { self.theme = theme self.title = title self.isHighlighted = isHighlighted + self.isEnabled = isEnabled self.progress = progress } @@ -68,6 +71,9 @@ final class AlertActionComponent: Component { private var component: AlertActionComponent? private weak var state: EmptyComponentState? + private var isEnabledDisposable: Disposable? + private var isEnabled = true + private var progressDisposable: Disposable? private var hasProgress = false @@ -84,12 +90,28 @@ final class AlertActionComponent: Component { preconditionFailure() } + deinit { + self.isEnabledDisposable?.dispose() + self.progressDisposable?.dispose() + } + 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.isEnabledDisposable = (component.isEnabled + |> deliverOnMainQueue).start(next: { [weak self] isEnabled in + guard let self else { + return + } + self.isEnabled = isEnabled + if !self.isUpdating { + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + }) + self.progressDisposable = (component.progress |> deliverOnMainQueue).start(next: { [weak self] hasProgress in guard let self else { @@ -161,8 +183,15 @@ final class AlertActionComponent: Component { } } + let buttonAlpha: CGFloat + if self.isEnabled { + buttonAlpha = component.isHighlighted ? 0.35 : 1.0 + } else { + buttonAlpha = 0.2 + } + transition.setBackgroundColor(view: self.backgroundView, color: component.theme.background) - transition.setAlpha(view: self.backgroundView, alpha: component.isHighlighted ? 0.35 : 1.0) + transition.setAlpha(view: self.backgroundView, alpha: buttonAlpha) self.backgroundView.layer.cornerRadius = availableSize.height * 0.5 return CGSize(width: titleSize.width + titlePadding * 2.0, height: availableSize.height) diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift index cc21864908..8c82ff746e 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -13,15 +13,23 @@ import GlassBackgroundComponent public final class AlertComponentEnvironment: Equatable { public let theme: PresentationTheme + public let strings: PresentationStrings - public init(theme: PresentationTheme) { + public init( + theme: PresentationTheme, + strings: PresentationStrings + ) { self.theme = theme + self.strings = strings } public static func ==(lhs: AlertComponentEnvironment, rhs: AlertComponentEnvironment) -> Bool { if lhs.theme !== rhs.theme { return false } + if lhs.strings !== rhs.strings { + return false + } return true } } @@ -297,10 +305,6 @@ private final class AlertScreenComponent: Component { 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 @@ -314,7 +318,7 @@ private final class AlertScreenComponent: Component { 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) + let alertEnvironment = AlertComponentEnvironment(theme: environment.theme, strings: environment.strings) var contentOriginY: CGFloat = 0.0 var validContentIds: Set = Set() @@ -348,6 +352,7 @@ private final class AlertScreenComponent: Component { if let itemView = item.view { if itemView.superview == nil { self.backgroundView.contentView.addSubview(itemView) + item.parentState = state } transition.setFrame(view: itemView, frame: itemFrame) } @@ -387,6 +392,8 @@ private final class AlertScreenComponent: Component { var effectiveActionLayout: ActionLayout = .horizontal if case .vertical = component.configuration.actionAlignment { effectiveActionLayout = .vertical + } else if component.actions.count == 1 { + effectiveActionLayout = .vertical } var validActionIds: Set = Set() for action in component.actions { @@ -421,7 +428,8 @@ private final class AlertScreenComponent: Component { theme: actionTheme, title: action.title, isHighlighted: AnyHashable(action.id) == self.highlightedAction, - progress: action.progressPromise.get() + isEnabled: action.isEnabled, + progress: action.progress )), environment: { alertEnvironment }, containerSize: fullWidthActionSize @@ -504,7 +512,12 @@ private final class AlertScreenComponent: Component { 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)) + var availableHeight = availableSize.height + if component.configuration.allowInputInset, environment.inputHeight > 0.0 { + availableHeight -= environment.inputHeight + } + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: floorToScreenPixels((availableHeight - 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 @@ -520,7 +533,7 @@ private final class AlertScreenComponent: Component { } } -public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { +open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { public enum ActionAligmnent: Equatable { case `default` case vertical @@ -532,22 +545,14 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder let allowInputInset: Bool public init( - actionAlignment: ActionAligmnent, - dismissOnOutsideTap: Bool, - allowInputInset: Bool + actionAlignment: ActionAligmnent = .default, + dismissOnOutsideTap: Bool = true, + allowInputInset: Bool = false ) { 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 { @@ -561,20 +566,23 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder public let type: ActionType public let action: () -> Void public let autoDismiss: Bool - public let progressPromise: ValuePromise + public let isEnabled: Signal + public let progress: Signal public init( title: String, type: ActionType = .generic, action: @escaping () -> Void = {}, autoDismiss: Bool = true, - progressPromise: ValuePromise = ValuePromise(false) + isEnabled: Signal = .single(true), + progress: Signal = .single(false) ) { self.type = type self.title = title self.action = action self.autoDismiss = autoDismiss - self.progressPromise = progressPromise + self.isEnabled = isEnabled + self.progress = progress } public static func ==(lhs: Action, rhs: Action) -> Bool { @@ -604,7 +612,7 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder public var dismissed: ((Bool) -> Void)? public init( - configuration: Configuration = .defaultValue, + configuration: Configuration = Configuration(), content: [AnyComponentWithIdentity], actions: [Action], updatedPresentationData: (initial: PresentationData, signal: Signal) @@ -630,12 +638,26 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder public convenience init( context: AccountContext, - configuration: Configuration = .defaultValue, + configuration: Configuration = Configuration(), content: [AnyComponentWithIdentity], actions: [Action] ) { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let updatedPresentationDataSignal = context.sharedContext.presentationData + self.init( + sharedContext: context.sharedContext, + configuration: configuration, + content: content, + actions: actions, + ) + } + + public convenience init( + sharedContext: SharedAccountContext, + configuration: Configuration = Configuration(), + content: [AnyComponentWithIdentity], + actions: [Action] + ) { + let presentationData = sharedContext.currentPresentationData.with { $0 } + let updatedPresentationDataSignal = sharedContext.presentationData self.init( configuration: configuration, content: content, @@ -645,7 +667,7 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder } public convenience init( - configuration: Configuration = .defaultValue, + configuration: Configuration = Configuration(), title: String? = nil, text: String, actions: [Action], @@ -660,12 +682,14 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder ) )) } - content.append(AnyComponentWithIdentity( - id: "text", - component: AnyComponent( - AlertTextComponent(content: .plain(text)) - ) - )) + if !text.isEmpty { + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + } self.init( configuration: configuration, @@ -677,7 +701,7 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder public convenience init( context: AccountContext, - configuration: Configuration = .defaultValue, + configuration: Configuration = Configuration(), title: String? = nil, text: String, actions: [Action] @@ -700,10 +724,6 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder deinit { } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - } - override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -719,7 +739,7 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder super.dismiss() } - override public func dismiss(completion: (() -> Void)? = nil) { + override open func dismiss(completion: (() -> Void)? = nil) { if !self.processedDidDisappear { self.processedDidDisappear = true diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift index c008872637..889d8a75d8 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift @@ -3,32 +3,53 @@ import UIKit import AsyncDisplayKit import Display import ComponentFlow +import TelegramCore import TelegramPresentationData import MultilineTextComponent +import MultilineTextWithEntitiesComponent import Markdown import TextFormat +import AccountContext private let titleFont = Font.bold(17.0) private let defaultTextFont = Font.regular(15.0) private let defaultBoldTextFont = Font.semibold(15.0) +private let defaultItalicTextFont = Font.italic(15.0) +private let defaultBoldItalicTextFont = Font.with(size: 15.0, weight: .semibold, traits: [.italic]) +private let defaultFixedTextFont = Font.monospace(15.0) private let smallTextFont = Font.regular(14.0) private let smallBoldTextFont = Font.semibold(14.0) +private let smallItalicTextFont = Font.italic(14.0) +private let smallBoldItalicTextFont = Font.with(size: 14.0, weight: .semibold, traits: [.italic]) +private let smallFixedTextFont = Font.monospace(14.0) +private let backgroundInset: CGFloat = 8.0 public final class AlertTitleComponent: Component { public typealias EnvironmentType = AlertComponentEnvironment + public enum Alignment { + case `default` + case center + } + let title: String + let alignment: Alignment public init( - title: String + title: String, + alignment: Alignment = .default ) { self.title = title + self.alignment = alignment } public static func ==(lhs: AlertTitleComponent, rhs: AlertTitleComponent) -> Bool { if lhs.title != rhs.title { return false } + if lhs.alignment != rhs.alignment { + return false + } return true } @@ -52,12 +73,21 @@ public final class AlertTitleComponent: Component { font: titleFont, textColor: environment.theme.actionSheet.primaryTextColor )), + horizontalAlignment: component.alignment == .center ? .center : .natural, maximumNumberOfLines: 0 )), environment: {}, containerSize: availableSize ) - let titleFrame = CGRect(origin: .zero, size: titleSize) + + let titleOriginX: CGFloat + switch component.alignment { + case .default: + titleOriginX = 0.0 + case .center: + titleOriginX = floorToScreenPixels((availableSize.width - titleSize.width) / 2.0) + } + let titleFrame = CGRect(origin: CGPoint(x: titleOriginX, y: 0.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) @@ -83,33 +113,71 @@ public final class AlertTextComponent: Component { public enum Content: Equatable { case plain(String) case attributed(NSAttributedString) + case textWithEntities(AccountContext, String, [MessageTextEntity]) + + public static func ==(lhs: Content, rhs: Content) -> Bool { + switch lhs { + case let .plain(text): + if case .plain(text) = rhs { + return true + } else { + return false + } + case let .attributed(text): + if case .attributed(text) = rhs { + return true + } else { + return false + } + case let .textWithEntities(_, lhsText, lhsEntities): + if case let .textWithEntities(_, rhsText, rhsEntities) = rhs { + return lhsText == rhsText && lhsEntities == rhsEntities + } else { + return false + } + } + } } - public enum Color { + public enum Alignment: Equatable { + case `default` + case center + } + + public enum Color: Equatable { case primary case secondary case destructive } - public enum TextSize { + public enum TextStyle: Equatable { case `default` case small + case bold + } + + public enum Style: Equatable { + case plain(TextStyle) + case background(TextStyle) } let content: Content + let alignment: Alignment let color: Color - let textSize: TextSize + let style: Style let action: ([NSAttributedString.Key: Any]) -> Void public init( content: Content, + alignment: Alignment = .default, color: Color = .primary, - textSize: TextSize = .default, + style: Style = .plain(.default), action: @escaping ([NSAttributedString.Key: Any]) -> Void = { _ in } ) { self.content = content + self.alignment = alignment self.color = color - self.textSize = textSize + self.style = style self.action = action } @@ -117,7 +185,10 @@ public final class AlertTextComponent: Component { if lhs.content != rhs.content { return false } - if lhs.textSize != rhs.textSize { + if lhs.alignment != rhs.alignment { + return false + } + if lhs.style != rhs.style { return false } if lhs.color != rhs.color { @@ -127,6 +198,7 @@ public final class AlertTextComponent: Component { } public final class View: UIView { + private let background = ComponentView() private let text = ComponentView() private var component: AlertTextComponent? @@ -151,16 +223,31 @@ public final class AlertTextComponent: Component { let textFont: UIFont let boldTextFont: UIFont - switch component.textSize { - case .default: - textFont = defaultTextFont - boldTextFont = defaultBoldTextFont - case .small: - textFont = smallTextFont - boldTextFont = smallBoldTextFont + let italicTextFont: UIFont + let fixedTextFont: UIFont + switch component.style { + case let .plain(textStyle), let .background(textStyle): + switch textStyle { + case .default: + textFont = defaultTextFont + boldTextFont = defaultBoldTextFont + italicTextFont = defaultItalicTextFont + fixedTextFont = defaultFixedTextFont + case .small: + textFont = smallTextFont + boldTextFont = smallBoldTextFont + italicTextFont = smallItalicTextFont + fixedTextFont = smallFixedTextFont + case .bold: + textFont = defaultBoldTextFont + boldTextFont = defaultBoldTextFont + italicTextFont = defaultBoldItalicTextFont + fixedTextFont = defaultFixedTextFont + } } var finalText: NSAttributedString + var context: AccountContext? switch component.content { case let .plain(text): let markdownAttributes = MarkdownAttributes( @@ -174,37 +261,92 @@ public final class AlertTextComponent: Component { finalText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes) case let .attributed(attributedText): finalText = attributedText + case let .textWithEntities(accountContext, text, entities): + context = accountContext + finalText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: linkColor, baseFont: textFont, linkFont: textFont, boldFont: boldTextFont, italicFont: italicTextFont, boldItalicFont: italicTextFont, fixedFont: fixedTextFont, blockQuoteFont: textFont, message: nil) } + var hasCenterAlignment = component.alignment == .center + switch component.style { + case .background: + hasCenterAlignment = true + default: + break + } + + let textConstrainedSize = CGSize(width: availableSize.width, height: availableSize.height) + 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 + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: context, + animationCache: context?.animationCache, + animationRenderer: context?.animationRenderer, + placeholderColor: textColor.withMultipliedAlpha(0.1), + text: .plain(finalText), + horizontalAlignment: hasCenterAlignment ? .center : .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + spoilerColor: textColor, + highlightColor: linkColor.withAlphaComponent(0.2), + manualVisibilityControl: true, + resetAnimationsOnVisibilityChange: true, + 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) } - }, - tapAction: { attributes, _ in - component.action(attributes) - } - )), + ) + ), environment: {}, - containerSize: availableSize + containerSize: textConstrainedSize ) - let textFrame = CGRect(origin: .zero, size: textSize) + + var textOffset = CGPoint() + if hasCenterAlignment { + textOffset.x = floorToScreenPixels((availableSize.width - textSize.width) / 2.0) + } + var size = CGSize(width: availableSize.width, height: textSize.height) + if case .background = component.style { + let backgroundSize = CGSize(width: availableSize.width + 20.0, height: textSize.height + backgroundInset * 2.0) + size = backgroundSize + textOffset = CGPoint(x: textOffset.x, y: backgroundInset) + + let _ = self.background.update( + transition: transition, + component: AnyComponent( + FilledRoundedRectangleComponent( + color: textColor.withMultipliedAlpha(0.1), + cornerRadius: .value(10.0), + smoothCorners: true + ) + ), + environment: {}, + containerSize: backgroundSize + ) + let backgroundFrame = CGRect(origin: CGPoint(x: -10.0, y: 0.0), size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + } + + let textFrame = CGRect(origin: textOffset, 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) + return size } } diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/LegacyAlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/LegacyAlertComponent.swift deleted file mode 100644 index d7b9b0668f..0000000000 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/LegacyAlertComponent.swift +++ /dev/null @@ -1,395 +0,0 @@ -import Foundation -import UIKit -import Display -import TelegramPresentationData -import ComponentFlow -import ComponentDisplayAdapters -import AsyncDisplayKit - -private let alertWidth: CGFloat = 270.0 - -public enum ComponentAlertActionType { - case genericAction - case defaultAction - case destructiveAction - case defaultDestructiveAction -} - -public struct ComponentAlertAction { - public let type: ComponentAlertActionType - public let title: String - public let action: () -> Void - - public init(type: ComponentAlertActionType, title: String, action: @escaping () -> Void) { - self.type = type - self.title = title - self.action = action - } -} - -public final class ComponentAlertContentActionNode: HighlightableButtonNode { - private var theme: AlertControllerTheme - public var action: ComponentAlertAction { - didSet { - self.updateTitle() - } - } - - private let backgroundNode: ASDisplayNode - - public var highlightedUpdated: (Bool) -> Void = { _ in } - - public init(theme: AlertControllerTheme, action: ComponentAlertAction) { - self.theme = theme - self.action = action - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.alpha = 0.0 - - super.init() - - self.titleNode.maximumNumberOfLines = 2 - - self.highligthedChanged = { [weak self] value in - if let strongSelf = self { - strongSelf.setHighlighted(value, animated: true) - } - } - - self.updateTheme(theme) - } - - public override func didLoad() { - super.didLoad() - - self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) - - self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in - if let strongSelf = self { - strongSelf.setHighlighted(true, animated: false) - } - }, willExit: { [weak self] in - if let strongSelf = self { - strongSelf.setHighlighted(false, animated: false) - } - }) - } - - public func performAction() { - if self.actionEnabled { - self.action.action() - } - } - - public func setHighlighted(_ highlighted: Bool, animated: Bool) { - self.highlightedUpdated(highlighted) - if highlighted { - if self.backgroundNode.supernode == nil { - self.insertSubnode(self.backgroundNode, at: 0) - } - self.backgroundNode.alpha = 1.0 - } else { - if animated { - UIView.animate(withDuration: 0.3, animations: { - self.backgroundNode.alpha = 0.0 - }) - } else { - self.backgroundNode.alpha = 0.0 - } - } - } - public var actionEnabled: Bool = true { - didSet { - self.isUserInteractionEnabled = self.actionEnabled - self.updateTitle() - } - } - - public func updateTheme(_ theme: AlertControllerTheme) { - self.theme = theme - self.backgroundNode.backgroundColor = theme.highlightedItemColor - self.updateTitle() - } - - private func updateTitle() { - var font = Font.regular(theme.baseFontSize) - var color: UIColor - switch self.action.type { - case .defaultAction, .genericAction: - color = self.actionEnabled ? self.theme.accentColor : self.theme.disabledColor - case .destructiveAction, .defaultDestructiveAction: - color = self.actionEnabled ? self.theme.destructiveColor : self.theme.disabledColor - } - switch self.action.type { - case .defaultAction, .defaultDestructiveAction: - font = Font.semibold(theme.baseFontSize) - case .destructiveAction, .genericAction: - break - } - self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: []) - self.accessibilityLabel = self.action.title - self.accessibilityTraits = [.button] - } - - @objc func pressed() { - self.action.action() - } - - override public func layout() { - super.layout() - - self.backgroundNode.frame = self.bounds - } -} - -public enum ComponentAlertContentActionLayout { - case horizontal - case vertical -} - -public final class ComponentAlertContentNode: AlertContentNode { - private var theme: AlertControllerTheme - private let actionLayout: ComponentAlertContentActionLayout - - private let content: AnyComponent - private let contentView = ComponentView() - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [ComponentAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - private let _dismissOnOutsideTap: Bool - override public var dismissOnOutsideTap: Bool { - return self._dismissOnOutsideTap - } - - private var highlightedItemIndex: Int? = nil - - public init(theme: AlertControllerTheme, content: AnyComponent, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout, dismissOnOutsideTap: Bool) { - self.theme = theme - self.actionLayout = actionLayout - self._dismissOnOutsideTap = dismissOnOutsideTap - self.content = content - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isUserInteractionEnabled = false - self.actionNodesSeparator.backgroundColor = theme.separatorColor - - self.actionNodes = actions.map { action -> ComponentAlertContentActionNode in - return ComponentAlertContentActionNode(theme: theme, action: action) - } - - var actionVerticalSeparators: [ASDisplayNode] = [] - if actions.count > 1 { - for _ in 0 ..< actions.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - separatorNode.backgroundColor = theme.separatorColor - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.actionNodesSeparator) - - var i = 0 - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - - let index = i - actionNode.highlightedUpdated = { [weak self] highlighted in - if highlighted { - self?.highlightedItemIndex = index - } - } - i += 1 - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - } - - func setHighlightedItemIndex(_ index: Int?, update: Bool = false) { - self.highlightedItemIndex = index - - if update { - var i = 0 - for actionNode in self.actionNodes { - if i == index { - actionNode.setHighlighted(true, animated: false) - } else { - actionNode.setHighlighted(false, animated: false) - } - i += 1 - } - } - } - - override public func decreaseHighlightedIndex() { - let currentHighlightedIndex = self.highlightedItemIndex ?? 0 - - self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true) - } - - override public func increaseHighlightedIndex() { - let currentHighlightedIndex = self.highlightedItemIndex ?? -1 - - self.setHighlightedItemIndex(min(self.actionNodes.count - 1, currentHighlightedIndex + 1), update: true) - } - - override public func performHighlightedAction() { - guard let highlightedItemIndex = self.highlightedItemIndex else { - return - } - - var i = 0 - for itemNode in self.actionNodes { - if i == highlightedItemIndex { - itemNode.performAction() - return - } - i += 1 - } - } - - override public func updateTheme(_ theme: AlertControllerTheme) { - self.theme = theme - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - self.validLayout = size - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) - - var size = size - size.width = min(size.width, alertWidth) - - let contentSize = self.contentView.update( - transition: ComponentTransition(transition), - component: self.content, - environment: {}, - containerSize: CGSize(width: size.width - insets.left - insets.right, height: 10000.0) - ) - - let actionButtonHeight: CGFloat = 44.0 - - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = self.actionLayout - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { - effectiveActionLayout = .vertical - } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } - } - - let resultSize: CGSize - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let contentWidth = alertWidth - insets.left - insets.right - - let contentFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - contentSize.width) / 2.0), y: insets.top), size: contentSize) - if let contentComponentView = self.contentView.view { - if contentComponentView.superview == nil { - self.view.insertSubview(contentComponentView, belowSubview: self.actionNodesSeparator.view) - transition.updateFrame(view: contentComponentView, frame: contentFrame) - } - } - - resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: contentSize.height + actionsHeight + insets.top + insets.bottom) - - self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - switch effectiveActionLayout { - case .horizontal: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - case .vertical: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - } - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical: - currentActionWidth = resultSize.width - } - - let actionNodeFrame: CGRect - switch effectiveActionLayout { - case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } - - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - return resultSize - } -} - -public func componentAlertController(theme: AlertControllerTheme, content: AnyComponent, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout = .horizontal, dismissOnOutsideTap: Bool = true) -> AlertController { - var dismissImpl: (() -> Void)? - let controller = AlertController(theme: theme, contentNode: ComponentAlertContentNode(theme: theme, content: content, actions: actions.map { action in - return ComponentAlertAction(type: action.type, title: action.title, action: { - dismissImpl?() - action.action() - }) - }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap)) - dismissImpl = { [weak controller] in - controller?.dismissAnimated() - } - return controller -} diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift index d1db40eefc..9d5bc66c52 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift @@ -212,7 +212,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, if case .audio = mode { if let savedMusic, savedMusic.count > 0 { - entries.append(.savedHeader(presentationData.theme, "SAVED MUSIC".uppercased())) + entries.append(.savedHeader(presentationData.theme, presentationData.strings.MediaEditor_Audio_SavedMusic.uppercased())) var savedMusic = savedMusic var showMore = false if savedMusic.count > 4 && !state.savedMusicExpanded { @@ -225,7 +225,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, i += 1 } if showMore { - entries.append(.showMore(presentationData.theme, "Show More")) + entries.append(.showMore(presentationData.theme, presentationData.strings.MediaEditor_Audio_ShowMore)) } } } @@ -542,8 +542,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre case .recent: title = presentationData.strings.Attachment_File case .audio: - //TODO:localize - title = "Audio" + title = presentationData.strings.MediaEditor_Audio_Title } let controllerState = ItemListControllerState( diff --git a/submodules/TelegramUI/Components/AvatarComponent/BUILD b/submodules/TelegramUI/Components/AvatarComponent/BUILD new file mode 100644 index 0000000000..f52981e856 --- /dev/null +++ b/submodules/TelegramUI/Components/AvatarComponent/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AvatarComponent", + module_name = "AvatarComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/ComponentFlow", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/AvatarNode", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AvatarComponent/Sources/AvatarComponent.swift b/submodules/TelegramUI/Components/AvatarComponent/Sources/AvatarComponent.swift new file mode 100644 index 0000000000..a9d18e4e4a --- /dev/null +++ b/submodules/TelegramUI/Components/AvatarComponent/Sources/AvatarComponent.swift @@ -0,0 +1,116 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import AvatarNode +import AccountContext + +public final class AvatarComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let peer: EnginePeer + let icon: AnyComponent? + + public init( + context: AccountContext, + theme: PresentationTheme, + peer: EnginePeer, + icon: AnyComponent? = nil + ) { + self.context = context + self.theme = theme + self.peer = peer + self.icon = icon + } + + public static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.icon != rhs.icon { + return false + } + return true + } + + public final class View: UIView { + private let avatarNode: AvatarNode + private var icon: ComponentView? + + private var component: AvatarComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + self.avatarNode.font = avatarPlaceholderFont(size: ceil(42.0 * availableSize.width / 100.0)) + } + + self.component = component + self.state = state + + var cutoutRect: CGRect? + if let icon = component.icon { + let iconView: ComponentView + if let current = self.icon { + iconView = current + } else { + iconView = ComponentView() + self.icon = iconView + } + let iconSize = iconView.update( + transition: .immediate, + component: icon, + environment: {}, + containerSize: availableSize + ) + let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - iconSize.width + 2.0, y: availableSize.height - iconSize.height + 2.0), size: iconSize) + if let iconView = iconView.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + cutoutRect = CGRect(origin: CGPoint(x: iconFrame.minX, y: availableSize.height - iconFrame.maxY), size: iconFrame.size).insetBy(dx: -2.0 + UIScreenPixel, dy: -2.0 + UIScreenPixel) + } + + self.avatarNode.frame = CGRect(origin: .zero, size: availableSize) + self.avatarNode.setPeer( + context: component.context, + theme: component.theme, + peer: component.peer, + synchronousLoad: true, + cutoutRect: cutoutRect + ) + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD b/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD index a87531995d..e31a01c3fa 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD @@ -42,6 +42,7 @@ swift_library( "//submodules/TelegramUI/Components/AvatarBackground", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/PremiumAlertController.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/PremiumAlertController.swift index eb8b314917..05d199a907 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/PremiumAlertController.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/PremiumAlertController.swift @@ -10,263 +10,117 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle +import AlertComponent import PremiumStarComponent -private final class PremiumAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let presentationTheme: PresentationTheme - private let title: String? - private let text: String - - private let titleNode: ASTextNode - private let textNode: ASTextNode - private let icon = ComponentView() - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init( - theme: AlertControllerTheme, - presentationTheme: PresentationTheme, - strings: PresentationStrings, - title: String?, - text: String, - actions: [TextAlertAction] - ) { - self.strings = strings - self.presentationTheme = presentationTheme - self.title = title - self.text = text - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 0 - self.textNode.lineSpacing = 0.1 - - 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.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title ?? self.strings.PremiumNeeded_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - 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 starHeight: CGFloat = 105.0 - let starSize = self.icon.update( - transition: .immediate, - component: AnyComponent( - PremiumStarComponent( - theme: self.presentationTheme, - isIntro: false, - isVisible: true, - hasIdleAnimations: true, - colors: [ - UIColor(rgb: 0x6a94ff), - UIColor(rgb: 0x9472fd), - UIColor(rgb: 0xe26bd3) - ] - ) - ), - environment: {}, - containerSize: CGSize(width: size.width, height: 200.0) - ) - if let view = self.icon.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.frame = CGRect(origin: CGPoint(x: 0.0, y: -36.0), size: starSize) - origin.y += starHeight - } - - let titleSize = self.titleNode.measure(size) - 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)) - - 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) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - let resultSize = CGSize(width: resultWidth, height: titleSize.height + starHeight + textSize.height + actionsHeight + 7.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 premiumAlertController(context: AccountContext, parentController: ViewController, title: String? = nil, text: String) -> AlertController { +func premiumAlertController( + context: AccountContext, + parentController: ViewController, + title: String? = nil, + text: String +) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - var dismissImpl: ((Bool) -> Void)? - var proceedImpl: (() -> Void)? - var contentNode: PremiumAlertContentNode? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.PremiumNeeded_Subscribe, action: { - proceedImpl?() - - dismissImpl?(true) - })] - - contentNode = PremiumAlertContentNode( - theme: AlertControllerTheme(presentationData: presentationData), - presentationTheme: presentationData.theme, - strings: strings, - title: title, - text: text, - actions: actions - ) + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertPremiumStarComponent() + ) + )) - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() + let title = title ?? strings.PremiumNeeded_Title + 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, + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.PremiumNeeded_Subscribe, type: .default, action: { [weak parentController] in + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .nameColor, forceDark: false, dismissed: nil) + parentController?.push(controller) + }) + ] + ) + return alertController +} + +private final class AlertPremiumStarComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public init() { + } + + public static func ==(lhs: AlertPremiumStarComponent, rhs: AlertPremiumStarComponent) -> Bool { + return true + } + + public final class View: UIView { + private let clippingView = UIView() + private let icon = ComponentView() + + private var component: AlertPremiumStarComponent? + private weak var state: EmptyComponentState? + + func update(component: AlertPremiumStarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let starHeight: CGFloat = 105.0 + let starSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + PremiumStarComponent( + theme: environment.theme, + isIntro: false, + isVisible: true, + hasIdleAnimations: true, + colors: [ + UIColor(rgb: 0x6a94ff), + UIColor(rgb: 0x9472fd), + UIColor(rgb: 0xe26bd3) + ] + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width + 60.0, height: 200.0) + ) + if let view = self.icon.view { + if view.superview == nil { + self.addSubview(self.clippingView) + self.clippingView.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 0.0, y: -24.0), size: starSize) + } + + self.clippingView.clipsToBounds = true + self.clippingView.layer.cornerRadius = 35.0 + self.clippingView.frame = CGRect(origin: CGPoint(x: -30.0, y: -22.0), size: CGSize(width: starSize.width, height: starSize.height)) + + return CGSize(width: availableSize.width, height: starHeight + 10.0) } } - proceedImpl = { [weak parentController] in - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .nameColor, forceDark: false, dismissed: nil) - parentController?.push(controller) + public func makeView() -> View { + return View(frame: CGRect()) } - return controller + 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/Chat/ChatAgeRestrictionAlertController/BUILD b/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/BUILD new file mode 100644 index 0000000000..c60929eefc --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatAgeRestrictionAlertController", + module_name = "ChatAgeRestrictionAlertController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/Sources/ChatAgeRestrictionAlertController.swift b/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/Sources/ChatAgeRestrictionAlertController.swift new file mode 100644 index 0000000000..cf87df8389 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/Sources/ChatAgeRestrictionAlertController.swift @@ -0,0 +1,64 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import AccountContext +import ComponentFlow +import AlertComponent +import AlertCheckComponent + +public func chatAgeRestrictionAlertController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)?, + parentController: ViewController, + completion: @escaping (Bool) -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let checkState = AlertCheckComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.SensitiveContent_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.SensitiveContent_Text)) + ) + )) + content.append(AnyComponentWithIdentity( + id: "check", + component: AnyComponent( + AlertCheckComponent(title: strings.SensitiveContent_ShowAlways, initialValue: false, externalState: checkState) + ) + )) + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) + } + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: strings.SensitiveContent_ViewAnyway, type: .default, action: { + completion(checkState.value) + }), + .init(title: strings.Common_Cancel) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + return alertController +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 21815278ab..9a835b50ac 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -488,6 +488,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { Queue.mainQueue().after(3.0, { self.labelNode.textNode.alpha = 1.0 self.labelBackgroundNode?.alpha = 1.0 + self.labelNode.textNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) + self.labelBackgroundNode?.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) self.labelNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) self.labelBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) }) @@ -1127,6 +1129,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var viewCount: Int? = nil var dateReplies = 0 var starsCount: Int64? + var tonAmount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -1145,6 +1148,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + if let stakeTonAmount = telegramDice?.tonAmount { + tonAmount = stakeTonAmount + } + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, associatedData: item.associatedData) var isReplyThread = false @@ -1173,6 +1180,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { messageEffect: messageEffect, replyCount: dateReplies, starsCount: starsCount, + tonAmount: tonAmount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.message), @@ -1277,6 +1285,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { quote: replyQuote, todoItemId: replyTodoItemId, story: replyStory, + isSummarized: false, parentMessage: item.message, constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude), animationCache: item.controllerInteraction.presentationContext.animationCache, @@ -1559,6 +1568,30 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + var updatedImageFrame: CGRect + var contextContentFrame: CGRect + if let _ = emojiString { + updatedImageFrame = imageFrame + contextContentFrame = updatedImageFrame.inset(by: UIEdgeInsets(top: 0.0, left: 0.0, bottom: -imageBottomPadding, right: 0.0)) + } else { + updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0)) + contextContentFrame = updatedImageFrame + + if let telegramDice, let _ = telegramDice.tonAmount { + updatedImageFrame = updatedImageFrame.offsetBy(dx: 0.0, dy: -30.0) + } + } + var updatedContentFrame = updatedImageFrame + if isEmoji && emojiString == nil { + updatedContentFrame = updatedContentFrame.insetBy(dx: -imageInset, dy: -imageInset) + contextContentFrame = updatedContentFrame + } + + let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: updatedImageFrame.maxY + 6.0), size: labelLayout.size) + strongSelf.labelNode.textNode.frame = labelFrame + if strongSelf.labelNode.textNode.supernode == nil, labelLayout.size.height > 0.0 { + strongSelf.addSubnode(strongSelf.labelNode.textNode) + } let _ = labelApply(TextNodeWithEntities.Arguments( context: item.context, @@ -1567,11 +1600,6 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, attemptSynchronous: synchronousLoads )) - let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size) - strongSelf.labelNode.textNode.frame = labelFrame - if strongSelf.labelNode.textNode.supernode == nil, labelLayout.size.height > 0.0 { - strongSelf.addSubnode(strongSelf.labelNode.textNode) - } let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) if let (offset, image) = backgroundMaskImage { @@ -1604,21 +1632,6 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.cachedMaskLabelBackgroundImage = (offset, image, labelRects) } - var updatedImageFrame: CGRect - var contextContentFrame: CGRect - if let _ = emojiString { - updatedImageFrame = imageFrame - contextContentFrame = updatedImageFrame.inset(by: UIEdgeInsets(top: 0.0, left: 0.0, bottom: -imageBottomPadding, right: 0.0)) - } else { - updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0)) - contextContentFrame = updatedImageFrame - } - var updatedContentFrame = updatedImageFrame - if isEmoji && emojiString == nil { - updatedContentFrame = updatedContentFrame.insetBy(dx: -imageInset, dy: -imageInset) - contextContentFrame = updatedContentFrame - } - if let (_, textApply) = textLayoutAndApply { let placeholderColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderColor, wallpaper: item.presentationData.theme.wallpaper) let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: placeholderColor, attemptSynchronous: synchronousLoads)) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 2d11aad170..a7b1e2ae15 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -689,6 +689,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var unlockButtonNode: ChatMessageUnlockMediaNode? private var mediaInfoNode: ChatMessageStarsMediaInfoNode? + private var summarizeButtonNode: ChatMessageShareButton? private var shareButtonNode: ChatMessageShareButton? private let messageAccessibilityArea: AccessibilityAreaNode @@ -1222,6 +1223,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + if let summarizeButtonNode = strongSelf.summarizeButtonNode, summarizeButtonNode.frame.contains(point) { + return .fail + } + if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) { return .fail } @@ -1724,6 +1729,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp()) var needsShareButton = false + var needsSummarizeButton = false if incoming, case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind { needsShareButton = true @@ -1755,6 +1761,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let channel = peer as? TelegramChannel { if case .broadcast = channel.info { needsShareButton = true + needsSummarizeButton = true } } } @@ -1789,6 +1796,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI loop: for media in item.message.media { if media is TelegramMediaAction { needsShareButton = false + needsSummarizeButton = false break loop } else if let media = media as? TelegramMediaFile, media.isInstantVideo { mayHaveSeparateCommentsButton = true @@ -1801,12 +1809,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if mayHaveSeparateCommentsButton && hasCommentButton(item: item) { } else { needsShareButton = false + needsSummarizeButton = false } } } if isPreview { needsShareButton = false + needsSummarizeButton = false } let isAd = item.content.firstMessage.adAttribute != nil if isAd { @@ -1815,12 +1825,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil { needsShareButton = false + needsSummarizeButton = false } } if let subject = item.associatedData.subject, case .messageOptions = subject { needsShareButton = false } + + #if DEBUG + + #else + needsSummarizeButton = false + #endif var tmpWidth: CGFloat if allowFullWidth { @@ -2717,7 +2734,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI hasReply = false } - if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) { + var isSummarized = false + if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) { + for attribute in item.message.attributes { + if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == "sum" { + isSummarized = true + hasReply = true + } + } + } + + if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil || isSummarized) { if headerSize.height.isZero { headerSize.height += 11.0 } else { @@ -2733,6 +2760,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI quote: replyQuote, todoItemId: replyTodoItemId, story: replyStory, + isSummarized: isSummarized, parentMessage: item.message, constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0, height: CGFloat.greatestFiniteMagnitude), animationCache: item.controllerInteraction.presentationContext.animationCache, @@ -3491,6 +3519,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI unlockButtonSizeAndApply: unlockButtonSizeApply, mediaInfoOrigin: mediaInfoOrigin?.offsetBy(dx: 0.0, dy: layoutInsets.top), mediaInfoSizeAndApply: mediaInfoSizeApply, + needsSummarizeButton: needsSummarizeButton, needsShareButton: needsShareButton, shareButtonOffset: shareButtonOffset, avatarOffset: avatarOffset, @@ -3556,6 +3585,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI unlockButtonSizeAndApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?), mediaInfoOrigin: CGPoint?, mediaInfoSizeAndApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?), + needsSummarizeButton: Bool, needsShareButton: Bool, shareButtonOffset: CGPoint?, avatarOffset: CGFloat?, @@ -4787,6 +4817,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI }) } + if needsSummarizeButton { + if strongSelf.summarizeButtonNode == nil { + let summarizeButtonNode = ChatMessageShareButton() + strongSelf.summarizeButtonNode = summarizeButtonNode + strongSelf.insertSubnode(summarizeButtonNode, belowSubnode: strongSelf.messageAccessibilityArea) + summarizeButtonNode.pressed = { [weak strongSelf] in + strongSelf?.toggleSummarization() + } + } + } else if let summarizeButtonNode = strongSelf.summarizeButtonNode { + strongSelf.summarizeButtonNode = nil + summarizeButtonNode.removeFromSupernode() + } + if needsShareButton { if strongSelf.shareButtonNode == nil { let shareButtonNode = ChatMessageShareButton() @@ -4960,6 +5004,33 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } strongSelf.messageAccessibilityArea.frame = backgroundFrame } + if let summarizeButtonNode = strongSelf.summarizeButtonNode { + let buttonSize = summarizeButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments, isSummarize: true) + + var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize) + if strongSelf.shareButtonNode != nil { + buttonFrame.origin.y -= buttonSize.width + 10.0 + } + + if let shareButtonOffset = shareButtonOffset { + if incoming { + buttonFrame.origin.x = shareButtonOffset.x + } + buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0) + } else if !disablesComments { + buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0) + } + + if isSidePanelOpen { + buttonFrame.origin.x -= buttonFrame.width * 0.5 + buttonFrame.origin.y += buttonFrame.height * 0.5 + } + + animation.animator.updatePosition(layer: summarizeButtonNode.layer, position: buttonFrame.center, completion: nil) + animation.animator.updateBounds(layer: summarizeButtonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil) + animation.animator.updateAlpha(layer: summarizeButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil) + animation.animator.updateScale(layer: summarizeButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil) + } if let shareButtonNode = strongSelf.shareButtonNode { let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments) @@ -4994,6 +5065,33 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.backgroundFrameTransition = nil }*/ strongSelf.messageAccessibilityArea.frame = backgroundFrame + if let summarizeButtonNode = strongSelf.summarizeButtonNode { + let buttonSize = summarizeButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments, isSummarize: true) + + var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize) + if strongSelf.shareButtonNode != nil { + buttonFrame.origin.y -= buttonSize.width + 10.0 + } + + if let shareButtonOffset = shareButtonOffset { + if incoming { + buttonFrame.origin.x = shareButtonOffset.x + } + buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0) + } else if !disablesComments { + buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0) + } + + if isSidePanelOpen { + buttonFrame.origin.x -= buttonFrame.width * 0.5 + buttonFrame.origin.y += buttonFrame.height * 0.5 + } + + animation.animator.updatePosition(layer: summarizeButtonNode.layer, position: buttonFrame.center, completion: nil) + animation.animator.updateBounds(layer: summarizeButtonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil) + animation.animator.updateAlpha(layer: summarizeButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil) + animation.animator.updateScale(layer: summarizeButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil) + } if let shareButtonNode = strongSelf.shareButtonNode { let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments) @@ -5275,6 +5373,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } else if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) { if let item = self.item { + if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) { + return .action(InternalBubbleTapAction.Action({ [weak self] in + guard let self else { + return + } + self.toggleSummarization() + })) + } + for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { if let threadId = item.message.threadId, Int32(clamping: threadId) == attribute.messageId.id, let quotedReply = item.message.attributes.first(where: { $0 is QuotedReplyMessageAttribute }) as? QuotedReplyMessageAttribute { @@ -5824,6 +5931,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return boostButtonNode.view } + if let summarizeButtonNode = self.summarizeButtonNode, summarizeButtonNode.frame.contains(point) { + return summarizeButtonNode.view.hitTest(self.view.convert(point, to: summarizeButtonNode.view), with: event) + } + if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view.hitTest(self.view.convert(point, to: shareButtonNode.view), with: event) } @@ -6516,6 +6627,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI container.updateAbsoluteRect(containerFrame, within: containerSize) } + if let summarizeButtonNode = self.summarizeButtonNode { + var summarizeButtonNodeFrame = summarizeButtonNode.frame + summarizeButtonNodeFrame.origin.x += rect.minX + summarizeButtonNodeFrame.origin.y += rect.minY + + summarizeButtonNode.updateAbsoluteRect(summarizeButtonNodeFrame, within: containerSize) + } + if let shareButtonNode = self.shareButtonNode { var shareButtonNodeFrame = shareButtonNode.frame shareButtonNodeFrame.origin.x += rect.minX @@ -6854,6 +6973,35 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI self.updateVisibility(isScroll: false) } + private func toggleSummarization() { + guard let item = self.item else { + return + } + + if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) { + item.controllerInteraction.summarizedMessageIds.remove(item.message.id) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, true) + } else { + item.controllerInteraction.summarizedMessageIds.insert(item.message.id) + + var needsSummarization = true + for attribute in item.message.attributes { + if let attribute = attribute as? TranslationMessageAttribute, attribute.toLang == "sum" { + needsSummarization = false + break + } + } + if needsSummarization { + let _ = (item.context.engine.messages.translateMessages(messageIds: [item.message.id], fromLang: nil, toLang: "sum", enableLocalIfPossible: false) + |> deliverOnMainQueue).start(completed: { + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, true) + }) + } else { + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, true) + } + } + } + private func updateVisibility(isScroll: Bool) { guard let item = self.item else { return diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 864920d628..d537ad2a1c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -12,6 +12,7 @@ import ReactionButtonListComponent import ReactionImageComponent import AnimationCache import MultiAnimationRenderer +import TelegramStringFormatting private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { if let _ = layer.animation(forKey: "clockFrameAnimation") { @@ -197,6 +198,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { var messageEffect: AvailableMessageEffects.MessageEffect? var replyCount: Int var starsCount: Int64? + var tonAmount: Int64? var isPinned: Bool var hasAutoremove: Bool var canViewReactionList: Bool @@ -222,6 +224,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { messageEffect: AvailableMessageEffects.MessageEffect?, replyCount: Int, starsCount: Int64?, + tonAmount: Int64? = nil, isPinned: Bool, hasAutoremove: Bool, canViewReactionList: Bool, @@ -246,6 +249,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { self.messageEffect = messageEffect self.replyCount = replyCount self.starsCount = starsCount + self.tonAmount = tonAmount self.isPinned = isPinned self.hasAutoremove = hasAutoremove self.canViewReactionList = canViewReactionList @@ -417,8 +421,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.incomingDateAndStatusPinnedIcon } - if (arguments.starsCount ?? 0) != 0 { + if (arguments.starsCount ?? 0) != 0 { starsImage = graphics.incomingDateAndStatusStarsIcon + } else if (arguments.tonAmount ?? 0) != 0 { + starsImage = graphics.incomingDateAndStatusTonIcon } case let .BubbleOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor @@ -438,6 +444,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } if (arguments.starsCount ?? 0) != 0 { starsImage = graphics.outgoingDateAndStatusStarsIcon + } else if (arguments.tonAmount ?? 0) != 0 { + starsImage = graphics.outgoingDateAndStatusTonIcon } case .ImageIncoming: dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor @@ -457,6 +465,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } if (arguments.starsCount ?? 0) != 0 { starsImage = graphics.mediaStarsIcon + } else if (arguments.tonAmount ?? 0) != 0 { + starsImage = graphics.mediaTonIcon } case let .ImageOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor @@ -477,6 +487,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } if (arguments.starsCount ?? 0) != 0 { starsImage = graphics.mediaStarsIcon + } else if (arguments.tonAmount ?? 0) != 0 { + starsImage = graphics.mediaTonIcon } case .FreeIncoming: let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) @@ -498,6 +510,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } if (arguments.starsCount ?? 0) != 0 { starsImage = graphics.freeStarsIcon + } else if (arguments.tonAmount ?? 0) != 0 { + starsImage = graphics.freeTonIcon } case let .FreeOutgoing(status): let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) @@ -519,6 +533,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } if (arguments.starsCount ?? 0) != 0 { starsImage = graphics.freeStarsIcon + } else if (arguments.tonAmount ?? 0) != 0 { + starsImage = graphics.freeTonIcon } } @@ -717,7 +733,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 - if arguments.starsCount != nil { + if arguments.starsCount != nil || arguments.tonAmount != nil { reactionInset += 3.0 } replyCountLayoutAndApply = layoutAndApply @@ -735,6 +751,11 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { countString = "\(starsCount)" } + let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) + reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 + starsCountLayoutAndApply = layoutAndApply + } else if let tonAmount = arguments.tonAmount, tonAmount > 0 { + let countString = formatTonAmountText(tonAmount, dateTimeFormat: arguments.presentationData.dateTimeFormat) let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 starsCountLayoutAndApply = layoutAndApply diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index f5bdcefb0d..c1ae3540ca 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -530,6 +530,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco quote: replyQuote, todoItemId: replyTodoItemId, story: replyStory, + isSummarized: false, parentMessage: item.message, constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude), animationCache: item.controllerInteraction.presentationContext.animationCache, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index b4605b04f0..6cf163da78 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -404,6 +404,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { quote: replyQuote, todoItemId: replyTodoItemId, story: replyStory, + isSummarized: false, parentMessage: item.message, constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), animationCache: item.controllerInteraction.presentationContext.animationCache, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift index 087a18ddaa..0c2b0da404 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift @@ -105,7 +105,7 @@ final class ChatCallNotificationItemNode: NotificationItemNode { override public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = width - let panelHeight: CGFloat = 66.0 + let panelHeight: CGFloat = 64.0 guard let item = self.item else { return panelHeight @@ -113,19 +113,19 @@ final class ChatCallNotificationItemNode: NotificationItemNode { let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } - let leftInset: CGFloat = 14.0 - let rightInset: CGFloat = 14.0 - let avatarSize: CGFloat = 38.0 + let leftInset: CGFloat = 12.0 + let rightInset: CGFloat = 12.0 + let avatarSize: CGFloat = 40.0 let avatarTextSpacing: CGFloat = 10.0 let buttonSpacing: CGFloat = 14.0 - let titleTextSpacing: CGFloat = 0.0 + let titleTextSpacing: CGFloat = 1.0 let maxTextWidth: CGFloat = width - leftInset - avatarTextSpacing - rightInset - avatarSize * 2.0 - buttonSpacing - avatarTextSpacing let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: maxTextWidth, height: 100.0) @@ -134,7 +134,7 @@ final class ChatCallNotificationItemNode: NotificationItemNode { let textSize = self.text.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: maxTextWidth, height: 100.0) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift index bf30319a26..466f3f0e1f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift @@ -381,11 +381,11 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { var applyImage: (() -> Void)? if let imageDimensions = imageDimensions { let boundingSize = CGSize(width: 55.0, height: 55.0) - var radius: CGFloat = 6.0 + var radius: CGFloat = 20.0 if isRound { radius = floor(boundingSize.width / 2.0) } - applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius, curve: isRound ? .circular : .continuous), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) } var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? @@ -424,16 +424,16 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { let compact = self.compact ?? false let panelHeight: CGFloat = compact ? 64.0 : 74.0 - let imageSize: CGSize = compact ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 54.0, height: 54.0) - let imageSpacing: CGFloat = compact ? 19.0 : 23.0 + let imageSize: CGSize = compact ? CGSize(width: 40.0, height: 40.0) : CGSize(width: 54.0, height: 54.0) + let imageSpacing: CGFloat = compact ? 22.0 : 23.0 let leftInset: CGFloat = imageSize.width + imageSpacing - var rightInset: CGFloat = 8.0 + var rightInset: CGFloat = 10.0 if !self.imageNode.isHidden { - rightInset += imageSize.width + 8.0 + rightInset += imageSize.width + 10.0 } - transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 10.0, y: (panelHeight - imageSize.height) / 2.0), size: imageSize)) + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 12.0, y: (panelHeight - imageSize.height) / 2.0), size: imageSize)) var titleInset: CGFloat = 0.0 if let image = self.titleIconNode.image { @@ -465,7 +465,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { let textSpacing: CGFloat = 1.0 - let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: 1.0 + floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size) transition.updateFrame(node: self.titleNode, frame: titleFrame) if let image = self.titleIconNode.image { @@ -475,7 +475,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size) transition.updateFrame(node: self.textNode.textNode, frame: textFrame) - transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 10.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize)) + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 12.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize)) if !textLayout.spoilers.isEmpty, let item = self.item { let presentationData = item.context.sharedContext.currentPresentationData.with({ $0 }) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/BUILD index ffec32b87e..d3f30ddc21 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/BUILD @@ -25,6 +25,8 @@ swift_library( "//submodules/CheckNode", "//submodules/TelegramUIPreferences", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent", "//submodules/Markdown", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift index ce6101d5cd..3bb04f3f39 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift @@ -16,309 +16,10 @@ import CheckNode import Markdown import TextFormat import StarsBalanceOverlayComponent +import AlertComponent +import AlertCheckComponent -private let textFont = Font.regular(13.0) -private let boldTextFont = Font.semibold(13.0) - -private func formattedText(_ text: String, fontSize: CGFloat, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { - return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: color), bold: MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: color), link: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: linkColor), linkAttribute: { _ in return (TelegramTextAttributes.URL, "") }), textAlignment: textAlignment) -} - -private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGestureRecognizerDelegate { - private let strings: PresentationStrings - private let title: String - private let text: String - private let optionText: String? - private let alignment: TextAlertContentActionLayout - - private let titleNode: ImmediateTextNode - private let textNode: ImmediateTextNode - - private let checkNode: InteractiveCheckNode - private let checkLabelNode: ImmediateTextNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - var dontAskAgain: Bool = false { - didSet { - self.checkNode.setSelected(self.dontAskAgain, animated: true) - - } - } - - var openTerms: () -> Void = {} - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) { - self.strings = strings - self.title = title - self.text = text - self.optionText = optionText - self.alignment = alignment - - self.titleNode = ImmediateTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.textAlignment = .center - - self.textNode = ImmediateTextNode() - self.textNode.maximumNumberOfLines = 0 - self.textNode.displaysAsynchronously = false - self.textNode.lineSpacing = 0.1 - self.textNode.textAlignment = .center - - self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) - self.checkLabelNode = ImmediateTextNode() - self.checkLabelNode.maximumNumberOfLines = 4 - - 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) - - if let _ = optionText { - self.addSubnode(self.checkNode) - self.addSubnode(self.checkLabelNode) - } - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.checkNode.valueChanged = { [weak self] value in - if let strongSelf = self { - strongSelf.dontAskAgain = !strongSelf.dontAskAgain - } - } - - self.checkLabelNode.highlightAttributeAction = { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - } - self.checkLabelNode.tapAttributeAction = { [weak self] attributes, _ in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - self?.openTerms() - } - } - - self.updateTheme(theme) - } - - override func didLoad() { - super.didLoad() - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:))) - tapGesture.delegate = self.wrappedGestureRecognizerDelegate - self.view.addGestureRecognizer(tapGesture) - } - - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let location = gestureRecognizer.location(in: self.checkLabelNode.view) - if self.checkLabelNode.bounds.contains(location) { - return true - } - return false - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - - if let (_, attributes) = self.checkLabelNode.attributesAtPoint(self.view.convert(point, to: self.checkLabelNode.view)) { - if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil { - return self.view - } - } - - return super.hitTest(point, with: event) - } - - @objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) { - self.dontAskAgain = !self.dontAskAgain - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = formattedText(self.text, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center) - - self.checkLabelNode.attributedText = parseMarkdownIntoAttributedString( - self.optionText ?? "", - attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor), - bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.primaryColor), - link: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor), - linkAttribute: { _ in - return nil - } - ) - ) - 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: 17.0) - - let titleSize = self.titleNode.updateLayout(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 - - var entriesHeight: CGFloat = 0.0 - - let textSize = self.textNode.updateLayout(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 - - if self.checkLabelNode.supernode != nil { - origin.y += 21.0 - entriesHeight += 21.0 - - let checkSize = CGSize(width: 22.0, height: 22.0) - let condensedSize = CGSize(width: size.width - 76.0, height: size.height) - - let spacing: CGFloat = 12.0 - let acceptTermsSize = self.checkLabelNode.updateLayout(condensedSize) - let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width - let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0) - - transition.updateFrame(node: self.checkNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize)) - transition.updateFrame(node: self.checkLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize)) - origin.y += acceptTermsSize.height - entriesHeight += acceptTermsSize.height - origin.y += 21.0 - } - - let actionButtonHeight: CGFloat = 44.0 - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = self.alignment - 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: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.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 - } -} - -public class ChatMessagePaymentAlertController: AlertController { +public class ChatMessagePaymentAlertController: AlertScreen { private let context: AccountContext? private let presentationData: PresentationData private weak var parentNavigationController: NavigationController? @@ -339,13 +40,14 @@ public class ChatMessagePaymentAlertController: AlertController { private let balance = ComponentView() private var didAppear = false - - private var validLayout: ContainerViewLayout? - + public init( context: AccountContext?, presentationData: PresentationData, - contentNode: AlertContentNode, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + configuration: Configuration = AlertScreen.Configuration(), + content: [AnyComponentWithIdentity], + actions: [AlertScreen.Action], navigationController: NavigationController?, chatPeerId: EnginePeer.Id, showBalance: Bool = true, @@ -360,28 +62,36 @@ public class ChatMessagePaymentAlertController: AlertController { self.currency = currency self.animateBalanceOverlay = animateBalanceOverlay - super.init(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - - self.willDismiss = { [weak self] in - guard let self else { - return - } - self.animateOut() + var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (initial: presentationData, signal: .single(presentationData)) } + + super.init( + configuration: configuration, + content: content, + actions: actions, + updatedPresentationData: effectiveUpdatedPresentationData + ) } required public init(coder aDecoder: NSCoder) { preconditionFailure() } + override public func dismiss(completion: (() -> Void)? = nil) { + super.dismiss(completion: completion) + + self.animateOut() + } + private func animateOut() { if !self.animateBalanceOverlay { if self.currency == .ton && self.didUpdateCurrency { self.currency = .stars } - Queue.mainQueue().after(0.39, { - - }) } else { if let view = self.balance.view { view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false) @@ -389,18 +99,10 @@ public class ChatMessagePaymentAlertController: AlertController { } } } - - public override func dismissAnimated() { - super.dismissAnimated() - self.animateOut() - } - public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - - self.validLayout = layout - + if !self.didAppear { self.didAppear = true if !layout.metrics.isTablet && layout.size.width > layout.size.height { @@ -452,7 +154,7 @@ public class ChatMessagePaymentAlertController: AlertController { } context.sharedContext.applicationBindings.openUrl(fragmentUrl) } - self.dismissAnimated() + self.dismiss(completion: nil) } ) ), @@ -486,57 +188,66 @@ public func chatMessagePaymentAlertController( hasCheck: Bool = true, navigationController: NavigationController?, completion: @escaping (Bool) -> Void -) -> AlertController { - let theme = defaultDarkColorPresentationTheme - let presentationData = updatedPresentationData?.initial ?? presentationData +) -> ViewController { let strings = presentationData.strings - var completionImpl: (() -> Void)? - var dismissImpl: (() -> Void)? - - let title = presentationData.strings.Chat_PaidMessage_Confirm_Title - let actionTitle = presentationData.strings.Chat_PaidMessage_Confirm_PayForMessage(count) - let messagesString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Messages(count) - - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: actionTitle, action: { - completionImpl?() - dismissImpl?() - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?() - })] - + let messagesString = strings.Chat_PaidMessage_Confirm_Text_Messages(count) let text: String if peers.count == 1, let peer = peers.first { - let amountString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value)) - let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count))) + let amountString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value)) + let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count))) if case let .channel(channel) = peer.chatOrMonoforumMainPeer, case .broadcast = channel.info { - text = presentationData.strings.Chat_PaidMessage_Confirm_SingleComment_Text(EnginePeer(channel).compactDisplayTitle, amountString, totalString, messagesString).string + text = strings.Chat_PaidMessage_Confirm_SingleComment_Text(EnginePeer(channel).compactDisplayTitle, amountString, totalString, messagesString).string } else { - text = presentationData.strings.Chat_PaidMessage_Confirm_Single_Text(peer.chatOrMonoforumMainPeer?.compactDisplayTitle ?? " ", amountString, totalString, messagesString).string + text = strings.Chat_PaidMessage_Confirm_Single_Text(peer.chatOrMonoforumMainPeer?.compactDisplayTitle ?? " ", amountString, totalString, messagesString).string } } else { let amount = totalAmount ?? amount - let usersString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count)) - let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count))) - text = presentationData.strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string + let usersString = strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count)) + let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count))) + text = strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string + } + + let checkState = AlertCheckComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Chat_PaidMessage_Confirm_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + if hasCheck { + content.append(AnyComponentWithIdentity( + id: "check", + component: AnyComponent( + AlertCheckComponent(title: strings.Chat_PaidMessage_Confirm_DontAskAgain, initialValue: false, externalState: checkState) + ) + )) } - let optionText = hasCheck ? presentationData.strings.Chat_PaidMessage_Confirm_DontAskAgain : nil - - let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .vertical) - - completionImpl = { [weak contentNode] in - guard let contentNode else { - return - } - completion(contentNode.dontAskAgain) - } - - let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, chatPeerId: context?.account.peerId ?? peers[0].peerId) - dismissImpl = { [weak controller] in - controller?.dismissAnimated() - } - return controller + let alertController = ChatMessagePaymentAlertController( + context: context, + presentationData: presentationData, + updatedPresentationData: updatedPresentationData, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: strings.Chat_PaidMessage_Confirm_PayForMessage(count), type: .default, action: { + completion(checkState.value) + }), + .init(title: strings.Common_Cancel) + ], + navigationController: navigationController, + chatPeerId: context?.account.peerId ?? peers[0].peerId + ) + return alertController } public func chatMessageRemovePaymentAlertController( @@ -548,26 +259,9 @@ public func chatMessageRemovePaymentAlertController( amount: StarsAmount?, navigationController: NavigationController?, completion: @escaping (Bool) -> Void -) -> AlertController { - let theme = defaultDarkColorPresentationTheme - let presentationData = updatedPresentationData?.initial ?? presentationData +) -> ViewController { let strings = presentationData.strings - var completionImpl: (() -> Void)? - var dismissImpl: (() -> Void)? - - let actions: [TextAlertAction] = [ - TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: { - dismissImpl?() - }), - TextAlertAction(type: .defaultAction, title: strings.Chat_PaidMessage_RemoveFee_Yes, action: { - completionImpl?() - dismissImpl?() - }) - ] - - let title = strings.Chat_PaidMessage_RemoveFee_Title - let text: String if case .user = chatPeer { text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string @@ -576,21 +270,44 @@ public func chatMessageRemovePaymentAlertController( } else { text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string } + + let checkState = AlertCheckComponent.ExternalState() - let optionText = amount.flatMap { strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32(clamping: $0.value))).string } - - let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .horizontal) - - completionImpl = { [weak contentNode] in - guard let contentNode else { - return - } - completion(contentNode.dontAskAgain) + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Chat_PaidMessage_RemoveFee_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + if let amount { + content.append(AnyComponentWithIdentity( + id: "check", + component: AnyComponent( + AlertCheckComponent(title: strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32(clamping: amount.value))).string, initialValue: false, externalState: checkState) + ) + )) } - let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, chatPeerId: chatPeer.id) - dismissImpl = { [weak controller] in - controller?.dismissAnimated() - } - return controller + let alertController = ChatMessagePaymentAlertController( + context: context, + presentationData: presentationData, + updatedPresentationData: updatedPresentationData, + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Chat_PaidMessage_RemoveFee_Yes, type: .default, action: { + completion(checkState.value) + }) + ], + navigationController: navigationController, + chatPeerId: chatPeer.id + ) + return alertController } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 9ebecccaad..3d2194bfe7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -85,6 +85,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { public let quote: (quote: EngineMessageReplyQuote, isQuote: Bool)? public let todoItemId: Int32? public let story: StoryId? + public let isSummarized: Bool public let parentMessage: Message public let constrainedSize: CGSize public let animationCache: AnimationCache? @@ -101,6 +102,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { quote: (quote: EngineMessageReplyQuote, isQuote: Bool)?, todoItemId: Int32?, story: StoryId?, + isSummarized: Bool, parentMessage: Message, constrainedSize: CGSize, animationCache: AnimationCache?, @@ -116,6 +118,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { self.quote = quote self.todoItemId = todoItemId self.story = story + self.isSummarized = isSummarized self.parentMessage = parentMessage self.constrainedSize = constrainedSize self.animationCache = animationCache @@ -437,7 +440,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } var textLeftInset: CGFloat = 0.0 - let messageText: NSAttributedString + var messageText: NSAttributedString var todoItemCompleted: Bool? if let todoItemId = arguments.todoItemId, let todo = arguments.message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) { messageText = stringWithAppliedEntities(todoItem.text, entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil) @@ -607,6 +610,12 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } adjustedConstrainedTextSize.width -= textLeftInset + if arguments.isSummarized { + //TODO:localize + titleString = NSAttributedString(string: "AI Summary", font: titleFont, textColor: titleColor) + messageText = NSAttributedString(string: "Tap to see original text", font: textFont, textColor: titleColor) + } + let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: maxTitleNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets)) if isExpiredStory || isStory { contrainedTextSize.width -= 26.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift b/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift index 502bd18b5b..981f902892 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift @@ -30,6 +30,7 @@ public class ChatMessageShareButton: ASDisplayNode { private var theme: PresentationTheme? private var isReplies: Bool = false private var hasMore: Bool = false + private var isExpand: Bool = false private var textNode: ImmediateTextNode? @@ -103,7 +104,7 @@ public class ChatMessageShareButton: ASDisplayNode { self.morePressed?() } - public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false) -> CGSize { + public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false, isSummarize: Bool = false) -> CGSize { var isReplies = false var isNavigate = false var replyCount = 0 @@ -134,15 +135,27 @@ public class ChatMessageShareButton: ASDisplayNode { hasMore = true } - if self.theme !== presentationData.theme.theme || self.isReplies != isReplies || self.hasMore != hasMore { + var isExpand = false + if controllerInteraction.summarizedMessageIds.contains(message.id) { + isExpand = true + } + + if self.theme !== presentationData.theme.theme || self.isReplies != isReplies || self.hasMore != hasMore || self.isExpand != isExpand { self.theme = presentationData.theme.theme self.isReplies = isReplies self.hasMore = hasMore + self.isExpand = isExpand var updatedIconImage: UIImage? var updatedBottomIconImage: UIImage? var updatedIconOffset = CGPoint() - if let _ = message.adAttribute { + if isSummarize { + if isExpand { + updatedIconImage = PresentationResourcesChat.chatFreeExpandButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + } else { + updatedIconImage = PresentationResourcesChat.chatFreeCollapseButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + } + } else if let _ = message.adAttribute { updatedIconImage = PresentationResourcesChat.chatFreeCloseButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) updatedIconOffset = CGPoint(x: UIScreenPixel, y: UIScreenPixel) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 6ab149a52c..5e3a781e9d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -777,6 +777,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { quote: replyQuote, todoItemId: replyTodoItemId, story: replyStory, + isSummarized: false, parentMessage: item.message, constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), animationCache: item.controllerInteraction.presentationContext.animationCache, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 2ed26e21d1..22ff90dbb6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -441,8 +441,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { messageEntities = updatingMedia.entities?.entities ?? [] } + var translateToLanguage = item.associatedData.translateToLanguage + if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) { + translateToLanguage = "sum" + } + if let subject = item.associatedData.subject, case .messageOptions = subject { - } else if let translateToLanguage = item.associatedData.translateToLanguage, !item.message.text.isEmpty && incoming { + } else if let translateToLanguage, !item.message.text.isEmpty && incoming { isTranslating = true for attribute in item.message.attributes { if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { diff --git a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD index 15cf263a28..09c44b1067 100644 --- a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD +++ b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD @@ -21,8 +21,9 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Components/BalancedTextComponent", "//submodules/Components/ComponentDisplayAdapters", - "//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TextFormat", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift index 0df9fdc4ec..9916fdfe22 100644 --- a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift @@ -13,399 +13,87 @@ import BalancedTextComponent import TextFieldComponent import ComponentDisplayAdapters import TextFormat +import ComponentFlow +import AlertComponent +import AlertMultilineInputFieldComponent -private final class FactCheckAlertContentNode: AlertContentNode { - private let context: AccountContext - private var theme: AlertControllerTheme - private var presentationTheme: PresentationTheme - private let strings: PresentationStrings - private let text: String - private let initialValue: String - - private let titleView = ComponentView() +public func factCheckAlertController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + value: String, + entities: [MessageTextEntity], + apply: @escaping (String, [MessageTextEntity]) -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let inputState = AlertMultilineInputFieldComponent.ExternalState() - private let state = ComponentState() - - private let inputBackgroundNode = ASImageNode() - private let inputField = ComponentView() - private let inputFieldExternalState = TextFieldComponent.ExternalState() - private let inputPlaceholderView = ComponentView() - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var present: (ViewController) -> () = { _ in } - - var complete: (() -> Void)? { - didSet { -// self.inputFieldNode.complete = self.complete + let doneIsEnabled: Signal + if !value.isEmpty { + doneIsEnabled = .single(true) + } else { + doneIsEnabled = inputState.valueSignal + |> map { value in + return !value.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled + var characterLimit: Int = 1024 + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["factcheck_length_limit"] as? Double { + characterLimit = Int(value) } - init(context: AccountContext, theme: AlertControllerTheme, presentationTheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, value: String, entities: [MessageTextEntity]) { - self.context = context - self.theme = theme - self.presentationTheme = presentationTheme - self.strings = strings - self.text = text - self.initialValue = value - - if !value.isEmpty { - self.inputFieldExternalState.initialText = chatInputStateStringWithAppliedEntities(value, entities: entities) - } - - 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.inputBackgroundNode.displaysAsynchronously = false - self.inputBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: presentationTheme.actionSheet.inputHollowBackgroundColor, strokeColor: presentationTheme.actionSheet.inputBorderColor, strokeWidth: UIScreenPixel) - - self.addSubnode(self.actionNodesSeparator) - self.addSubnode(self.inputBackgroundNode) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - self.actionNodes.last?.actionEnabled = true - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - - self.state._updated = { [weak self] transition, _ in - guard let self, let _ = self.validLayout else { - return - } - self.requestLayout?(transition.containedViewLayoutTransition) - } - } + let initialValue = chatInputStateStringWithAppliedEntities(value, entities: entities) - deinit { - self.disposable.dispose() - } - - var textAndEntities: (String, [MessageTextEntity]) { - let text = self.inputFieldExternalState.text.string - let entities = generateChatInputTextEntities(self.inputFieldExternalState.text) - return (text, entities) - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.theme = theme - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 16.0) - let spacing: CGFloat = 5.0 - - - let titleSize = self.titleView.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: measureSize.width, height: 1000.0) + var presentImpl: ((ViewController) -> Void)? + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.FactCheck_Title) ) - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: origin.y), size: titleSize) - if let titleComponentView = self.titleView.view { - if titleComponentView.superview == nil { - self.view.addSubview(titleComponentView) - } - titleComponentView.frame = titleFrame - } - origin.y += titleSize.height + 17.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: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputInset: CGFloat = 16.0 - let inputWidth = resultWidth - inputInset * 2.0 - - var characterLimit: Int = 1024 - if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["factcheck_length_limit"] as? Double { - characterLimit = Int(value) - } - - let inputFieldSize = self.inputField.update( - transition: .immediate, - component: AnyComponent(TextFieldComponent( - context: self.context, - theme: self.presentationTheme, - strings: self.strings, - externalState: self.inputFieldExternalState, - fontSize: 14.0, - textColor: self.presentationTheme.actionSheet.inputTextColor, - accentColor: self.presentationTheme.actionSheet.controlAccentColor, - insets: UIEdgeInsets(top: 8.0, left: 2.0, bottom: 8.0, right: 2.0), - hideKeyboard: false, - customInputView: nil, - resetText: nil, - isOneLineWhenUnfocused: false, + )) + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertMultilineInputFieldComponent( + context: context, + initialValue: initialValue, + placeholder: strings.FactCheck_Placeholder, characterLimit: characterLimit, + formatMenuAvailability: .available([.bold, .italic]), emptyLineHandling: .oneConsecutive, - formatMenuAvailability: .available([.bold, .italic, .link]), - returnKeyType: .default, - lockedFormatAction: { - }, - present: { [weak self] c in - self?.present(c) - }, - paste: { _ in - }, - returnKeyAction: nil, - backspaceKeyAction: nil - )), - environment: {}, - containerSize: CGSize(width: inputWidth, height: 270.0) - ) - self.inputField.parentState = self.state - let inputFieldFrame = CGRect(origin: CGPoint(x: inputInset, y: origin.y), size: inputFieldSize) - if let inputFieldView = self.inputField.view as? TextFieldComponent.View { - if inputFieldView.superview == nil { - self.view.addSubview(inputFieldView) - } - transition.updateFrame(view: inputFieldView, frame: inputFieldFrame) - transition.updateFrame(node: self.inputBackgroundNode, frame: inputFieldFrame) - - if !hadValidLayout { - inputFieldView.activateInput() - } - } - - let inputPlaceholderSize = self.inputPlaceholderView.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString( - string: self.strings.FactCheck_Placeholder, - font: Font.regular(14.0), - textColor: self.presentationTheme.actionSheet.inputPlaceholderColor - ))) - ), - environment: {}, - containerSize: CGSize(width: inputWidth, height: 240.0) - ) - let inputPlaceholderFrame = CGRect(origin: CGPoint(x: inputInset + 10.0, y: floorToScreenPixels(inputFieldFrame.midY - inputPlaceholderSize.height / 2.0)), size: inputPlaceholderSize) - if let inputPlaceholderView = self.inputPlaceholderView.view { - if inputPlaceholderView.superview == nil { - inputPlaceholderView.isUserInteractionEnabled = false - self.view.addSubview(inputPlaceholderView) - } - inputPlaceholderView.frame = inputPlaceholderFrame - inputPlaceholderView.isHidden = self.inputFieldExternalState.hasText - } - - if let lastActionNode = self.actionNodes.last { - if self.initialValue.isEmpty { - lastActionNode.actionEnabled = self.inputFieldExternalState.hasText - } else { - if self.inputFieldExternalState.hasText { - lastActionNode.action = TextAlertAction( - type: .defaultAction, - title: self.strings.Common_Done, - action: lastActionNode.action.action - ) - } else { - lastActionNode.action = TextAlertAction( - type: .defaultDestructiveAction, - title: self.strings.FactCheck_Remove, - action: lastActionNode.action.action - ) + isInitiallyFocused: true, + externalState: inputState, + present: { c in + presentImpl?(c) } - } - } - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + spacing + inputFieldSize.height + 17.0 + actionsHeight + 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 deactivateInput() { - if let inputFieldView = self.inputField.view as? TextFieldComponent.View { - inputFieldView.deactivateInput() - } + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } - - func animateError() { - if let inputFieldView = self.inputField.view as? TextFieldComponent.View { - inputFieldView.layer.addShakeAnimation() - } - - self.hapticFeedback.error() + //FactCheck_Remove + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Common_Done, type: .default, action: { + let (text, entities) = inputState.textAndEntities + apply(text, entities) + }, isEnabled: doneIsEnabled) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + presentImpl = { [weak alertController] c in + alertController?.present(c, in: .window(.root)) } -} - -public func factCheckAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, value: String, entities: [MessageTextEntity], apply: @escaping (String, [MessageTextEntity]) -> Void) -> AlertController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - - var dismissImpl: ((Bool) -> Void)? - var applyImpl: (() -> Void)? - - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { - dismissImpl?(true) - applyImpl?() - })] - - let contentNode = FactCheckAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), presentationTheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: presentationData.strings.FactCheck_Title, value: value, entities: entities) - contentNode.complete = { - applyImpl?() - } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - let (text, entities) = contentNode.textAndEntities - apply(text, entities) - } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller] animated in - contentNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - - contentNode.present = { [weak controller] c in - controller?.present(c, in: .window(.root)) - } - - return controller + return alertController } diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 6855559492..894a9d0f5f 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -313,6 +313,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public var enableFullTranslucency: Bool = true public var chatIsRotated: Bool = true public var canReadHistory: Bool = false + public var summarizedMessageIds: Set = Set() + private var isOpeningMediaValue: Bool = false public var isOpeningMedia: Bool { diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/BUILD b/submodules/TelegramUI/Components/ChatThemeScreen/BUILD index 783bf6048c..cb29f70424 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatThemeScreen/BUILD @@ -37,6 +37,9 @@ swift_library( "//submodules/AppBundle", "//submodules/ActivityIndicator", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", + "//submodules/TelegramUI/Components/AvatarComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index 589c5c5582..3f851686dd 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -22,6 +22,7 @@ import TelegramAnimatedStickerNode import ShimmerEffect import AttachmentUI import AvatarNode +import AlertComponent private struct ThemeSettingsThemeEntry: Comparable, Identifiable { let index: Int @@ -925,7 +926,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) self.animationNode.isUserInteractionEnabled = false - self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 50.0, cornerRadius: 11.0) + self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), glass: true, height: 52.0, cornerRadius: 26.0) self.otherButton = HighlightableButtonNode() @@ -1530,7 +1531,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { var presentingAlertController = false self.controller?.forEachController({ c in - if c is AlertController { + if c is AlertScreen { presentingAlertController = true } return true @@ -1605,13 +1606,14 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: cancelSize) transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame) + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: layout.intrinsicInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonInset: CGFloat = 16.0 - let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) + let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInsets.left - buttonInsets.right, transition: transition) var doneY = contentHeight - doneButtonHeight - 2.0 - insets.bottom if self.controller?.canResetWallpaper == true { doneY = contentHeight - doneButtonHeight - 52.0 - insets.bottom } - transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: doneY, width: contentFrame.width, height: doneButtonHeight)) + transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInsets.left, y: doneY, width: contentFrame.width, height: doneButtonHeight)) let otherButtonSize = self.otherButton.measure(CGSize(width: contentFrame.width - buttonInset * 2.0, height: .greatestFiniteMagnitude)) self.otherButton.frame = CGRect(origin: CGPoint(x: floor((contentFrame.width - otherButtonSize.width) / 2.0), y: contentHeight - otherButtonSize.height - insets.bottom - 15.0), size: otherButtonSize) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift index 03ea78002f..7b4a490783 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift @@ -3,293 +3,65 @@ import UIKit import AsyncDisplayKit import Display import ComponentFlow -import Postbox import TelegramCore import TelegramPresentationData -import TelegramUIPreferences import AccountContext -import AppBundle -import AvatarNode -import Markdown +import AlertComponent +import AlertTransferHeaderComponent import GiftItemComponent -import ActivityIndicator - -private final class GiftThemeTransferAlertContentNode: 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 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: "Media Editor/CutoutUndo"), 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) - } - } - - 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 giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 52.0, y: origin.y), size: avatarSize) - - let _ = self.giftView.update( - transition: .immediate, - component: AnyComponent( - GiftItemComponent( - context: self.context, - theme: self.presentationTheme, - strings: self.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) + 52.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 += 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 - - let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 24.0 + insets.top + insets.bottom) - self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - - let actionNodeFrame: CGRect - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - - 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.isHidden = true - - 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 AvatarComponent public func giftThemeTransferAlertController( context: AccountContext, gift: StarGift.UniqueGift, previousPeer: EnginePeer, commit: @escaping () -> Void -) -> AlertController { +) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - - var contentNode: GiftThemeTransferAlertContentNode? - var dismissImpl: ((Bool) -> Void)? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Conversation_Theme_GiftTransfer_Proceed, action: { - dismissImpl?(true) - commit() - })] - let text = strings.Conversation_Theme_GiftTransfer_Text(previousPeer.compactDisplayTitle).string - contentNode = GiftThemeTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: previousPeer, title: "", text: text, actions: actions) + 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: "user", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: previousPeer + ) + )), + type: .take + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Conversation_Theme_GiftTransfer_Text(previousPeer.compactDisplayTitle).string)) + ) + )) - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + let alertController = AlertScreen( + context: context, + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Conversation_Theme_GiftTransfer_Proceed, type: .default, action: { + commit() + }) + ] + ) + return alertController } diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift index 49e1b27f26..78a6b22f96 100644 --- a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift @@ -256,14 +256,7 @@ private final class SheetContent: CombinedComponent { AmountPresetsListItemComponent( context: component.context, theme: theme, - values: [ - 100000000, - 1000000000, - 2000000000, - 5000000000, - 10000000000, - 20000000000 - ], + values: configuration.suggestedAmounts, valueSelected: { [weak state] value in guard let state else { return @@ -1595,13 +1588,36 @@ private final class AmountPresetComponent: Component { private struct EmojiGameStakeConfiguration { static var defaultValue: EmojiGameStakeConfiguration { - return EmojiGameStakeConfiguration(tonUsdRate: nil) + return EmojiGameStakeConfiguration( + tonUsdRate: nil, + minStakeAmount: nil, + maxStakeAmount: nil, + suggestedAmounts: [ + 100000000, + 1000000000, + 2000000000, + 5000000000, + 10000000000, + 20000000000 + ] + ) } let tonUsdRate: Double? - - fileprivate init(tonUsdRate: Double?) { + let minStakeAmount: Int64? + let maxStakeAmount: Int64? + let suggestedAmounts: [Int64] + + fileprivate init( + tonUsdRate: Double?, + minStakeAmount: Int64?, + maxStakeAmount: Int64?, + suggestedAmounts: [Int64] + ) { self.tonUsdRate = tonUsdRate + self.minStakeAmount = minStakeAmount + self.maxStakeAmount = maxStakeAmount + self.suggestedAmounts = suggestedAmounts } static func with(appConfiguration: AppConfiguration) -> EmojiGameStakeConfiguration { @@ -1611,7 +1627,29 @@ private struct EmojiGameStakeConfiguration { tonUsdRate = value } - return EmojiGameStakeConfiguration(tonUsdRate: tonUsdRate) + var minStakeAmount: Int64? + if let value = data["ton_stakedice_stake_amount_min"] as? Double { + minStakeAmount = Int64(value) + } + + var maxStakeAmount: Int64? + if let value = data["ton_stakedice_stake_amount_max"] as? Double { + maxStakeAmount = Int64(value) + } + + var suggestedAmounts: [Int64] = [] + if let value = data["ton_stakedice_stake_suggested_amounts"] as? [Double] { + suggestedAmounts = value.map { Int64($0) } + } else { + suggestedAmounts = EmojiGameStakeConfiguration.defaultValue.suggestedAmounts + } + + return EmojiGameStakeConfiguration( + tonUsdRate: tonUsdRate, + minStakeAmount: minStakeAmount, + maxStakeAmount: maxStakeAmount, + suggestedAmounts: suggestedAmounts + ) } else { return .defaultValue } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD index 558f8cfb5a..089497fb3f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD @@ -46,6 +46,9 @@ swift_library( "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", + "//submodules/TelegramUI/Components/AvatarComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift index 48ad09ede5..3344febc30 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftAuctionTransferController.swift @@ -9,225 +9,8 @@ import TelegramPresentationData import AccountContext import AppBundle 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 -// -// 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 -// } -//} +import AlertTransferHeaderComponent +import AvatarComponent func giftAuctionTransferController(context: AccountContext, fromPeer: EnginePeer, toPeer: EnginePeer, commit: @escaping () -> Void) -> AlertScreen { let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -244,27 +27,28 @@ func giftAuctionTransferController(context: AccountContext, fromPeer: EnginePeer } 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: "header", + component: AnyComponent( + AlertTransferHeaderComponent( + fromComponent: AnyComponentWithIdentity(id: "fromPeer", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: fromPeer + ) + )), + toComponent: AnyComponentWithIdentity(id: "toPeer", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: toPeer + ) + )), + type: .transfer + ) + ) + )) content.append(AnyComponentWithIdentity( id: "title", component: AnyComponent( @@ -290,26 +74,4 @@ func giftAuctionTransferController(context: AccountContext, fromPeer: EnginePeer ] ) 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 } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index cf236d2fe6..65f74cb098 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -67,6 +67,9 @@ swift_library( "//submodules/TelegramUI/Components/Gifts/TableComponent", "//submodules/TelegramUI/Components/Gifts/PeerTableCellComponent", "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AvatarComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertTableComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 2a19fa7de7..3832c9f294 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -18,7 +18,6 @@ import RoundedRectWithTailPath import AvatarNode import BundleIconComponent import TextFormat -import CheckComponent import ContextUI import StarsBalanceOverlayComponent import StoryLiveChatMessageComponent diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift index ba54c9b261..b4e39c8b51 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift @@ -296,8 +296,7 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: AnyComponentWithIdentity( id: AnyHashable(0), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift index e90791c64f..8198689cff 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift @@ -26,6 +26,7 @@ import GiftAnimationComponent import GlassBarButtonComponent import GiftRemainingCountComponent import AnimatedTextComponent +import AvatarComponent private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift index 69cdbadcd2..7b7409f4db 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift @@ -16,6 +16,9 @@ import BundleIconComponent import TelegramStringFormatting import AlertComponent import TableComponent +import AvatarComponent +import AlertTransferHeaderComponent +import AlertTableComponent public func giftOfferAlertController( context: AccountContext, @@ -160,7 +163,8 @@ public func giftOfferAlertController( theme: presentationData.theme, peer: peer ) - )) + )), + type: .transfer ) ) )) @@ -204,7 +208,7 @@ public func giftOfferAlertController( content.append(AnyComponentWithIdentity( id: "warning", component: AnyComponent( - AlertTextComponent(content: .plain(warningText), color: .destructive, textSize: .small) + AlertTextComponent(content: .plain(warningText), color: .destructive, style: .plain(.small)) ) )) } @@ -254,166 +258,3 @@ public func giftOfferAlertController( } 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/GiftPurchaseAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift index 805bf3bf76..4d1729352b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift @@ -470,16 +470,20 @@ public func giftPurchaseAlertController( contentNode = GiftPurchaseAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), presentationTheme: presentationData.theme, strings: strings, gift: gift, peer: peer, actions: actions) - let controller = ChatMessagePaymentAlertController( - context: context, - presentationData: presentationData, - contentNode: contentNode!, - navigationController: navigationController, - chatPeerId: context.account.peerId, - showBalance: true, - currency: gift.resellForTonOnly ? .ton : .stars, - animateBalanceOverlay: animateBalanceOverlay - ) +// let controller = ChatMessagePaymentAlertController( +// context: context, +// presentationData: presentationData, +// contentNode: contentNode!, +// navigationController: navigationController, +// chatPeerId: context.account.peerId, +// showBalance: true, +// currency: gift.resellForTonOnly ? .ton : .stars, +// animateBalanceOverlay: animateBalanceOverlay +// ) + + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + controller.dismissed = { _ in dismissed() } @@ -497,7 +501,8 @@ public func giftPurchaseAlertController( } contentNode?.updatedCurrency = { [weak controller] currency in - controller?.currency = currency + let _ = controller + //controller?.currency = currency } if !gift.resellForTonOnly { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift index 8dfe60413b..f461e20c20 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift @@ -9,300 +9,11 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle -import Markdown import ChatMessagePaymentAlertController -import ActivityIndicator -import MultilineTextWithEntitiesComponent import TelegramStringFormatting import TextFormat import AlertComponent -private final class GiftRemoveInfoAlertContentNode: 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 peers: [EnginePeer.Id: EnginePeer] - - private let titleNode: ASTextNode - private let textNode: ASTextNode - private let infoBackgroundNode: ASDisplayNode - private let infoView = ComponentView() - - 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, - peers: [EnginePeer.Id: 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.peers = peers - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 0 - - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 0 - - self.infoBackgroundNode = ASDisplayNode() - self.infoBackgroundNode.backgroundColor = ptheme.overallDarkAppearance ? ptheme.list.itemModalBlocksBackgroundColor : ptheme.list.itemPrimaryTextColor.withAlphaComponent(0.04) - self.infoBackgroundNode.cornerRadius = 10.0 - self.infoBackgroundNode.displaysAsynchronously = false - - 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.infoBackgroundNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - } - - 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.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, 310.0) - - let strings = self.strings - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.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) - - var infoSize: CGSize = .zero - for attribute in self.gift.attributes { - if case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities) = attribute { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - - let tableFont = Font.regular(13.0) - let tableBoldFont = Font.semibold(13.0) - let tableItalicFont = Font.italic(13.0) - let tableBoldItalicFont = Font.semiboldItalic(13.0) - let tableMonospaceFont = Font.monospace(13.0) - - let tableTextColor = self.presentationTheme.list.itemPrimaryTextColor - let tableLinkColor = self.presentationTheme.list.itemAccentColor - - let senderName = senderPeerId.flatMap { self.peers[$0]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) } - let recipientName = self.peers[recipientPeerId]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) ?? "" - - let dateString = stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: presentationData.dateTimeFormat, withTime: false) - let value: NSAttributedString - if let text { - let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil) - - let format = senderName != nil ? presentationData.strings.Gift_Unique_OriginalInfoSenderWithText(senderName!, recipientName, dateString, "") : presentationData.strings.Gift_Unique_OriginalInfoWithText(recipientName, dateString, "") - let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor) - string.replaceCharacters(in: format.ranges[format.ranges.count - 1].range, with: attributedText) - if let _ = senderPeerId { - string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) - string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) - } else { - string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) - } - value = string - } else { - let format = senderName != nil ? presentationData.strings.Gift_Unique_OriginalInfoSender(senderName!, recipientName, dateString) : presentationData.strings.Gift_Unique_OriginalInfo(recipientName, dateString) - let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor) - if let _ = senderPeerId { - string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) - string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) - } else { - string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) - } - - value = string - } - - infoSize = self.infoView.update( - transition: .immediate, - component: AnyComponent( - MultilineTextWithEntitiesComponent( - context: self.context, - animationCache: self.context.animationCache, - animationRenderer: self.context.animationRenderer, - placeholderColor: self.presentationTheme.list.mediaPlaceholderColor, - text: .plain(value), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - handleSpoilers: true - ) - ), - environment: {}, - containerSize: CGSize(width: contentWidth - 64.0, height: size.height) - ) - let infoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - infoSize.width) / 2.0), y: titleSize.height + textSize.height + 54.0), size: infoSize) - if let view = self.infoView.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.frame = infoFrame - } - self.infoBackgroundNode.frame = infoFrame.insetBy(dx: -12.0, dy: -12.0) - - break - } - } - - let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + infoSize.height + actionsHeight + 46.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.isHidden = true - - let indicatorSize = CGSize(width: 22.0, height: 22.0) - transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize)) - } - } - - return resultSize - } -} - public func giftRemoveInfoAlertController( context: AccountContext, gift: StarGift.UniqueGift, @@ -310,32 +21,89 @@ public func giftRemoveInfoAlertController( removeInfoStars: Int64, navigationController: NavigationController?, commit: @escaping () -> Void -) -> AlertController { +) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - let title = strings.Gift_RemoveDetails_Title - let text = strings.Gift_RemoveDetails_Text - let buttonText = strings.Gift_RemoveDetails_Action(" $ \(presentationStringsFormattedNumber(Int32(clamping: removeInfoStars), presentationData.dateTimeFormat.groupingSeparator))").string - - var contentNode: GiftRemoveInfoAlertContentNode? - var dismissImpl: ((Bool) -> Void)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { - dismissImpl?(true) - commit() - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - })] + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Gift_RemoveDetails_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Gift_RemoveDetails_Text)) + ) + )) - contentNode = GiftRemoveInfoAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peers: peers, title: title, text: text, actions: actions) - - let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: navigationController, chatPeerId: context.account.peerId, showBalance: removeInfoStars > 0) - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() + for attribute in gift.attributes { + if case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities) = attribute { + let textColor = presentationData.theme.actionSheet.primaryTextColor + let linkColor = presentationData.theme.actionSheet.controlAccentColor + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let italicTextFont = Font.italic(15.0) + let boldItalicTextFont = Font.with(size: 15.0, weight: .semibold, traits: .italic) + let fixedTextFont = Font.monospace(15.0) + + let senderName = senderPeerId.flatMap { peers[$0]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) } + let recipientName = peers[recipientPeerId]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) ?? "" + + let dateString = stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: presentationData.dateTimeFormat, withTime: false) + let value: NSAttributedString + if let text { + let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: textColor, linkColor: linkColor, baseFont: textFont, linkFont: textFont, boldFont: boldTextFont, italicFont: italicTextFont, boldItalicFont: boldItalicTextFont, fixedFont: fixedTextFont, blockQuoteFont: textFont, message: nil) + + let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSenderWithText(senderName!, recipientName, dateString, "") : strings.Gift_Unique_OriginalInfoWithText(recipientName, dateString, "") + let string = NSMutableAttributedString(string: format.string, font: textFont, textColor: textColor) + string.replaceCharacters(in: format.ranges[format.ranges.count - 1].range, with: attributedText) + if let _ = senderPeerId { + string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[0].range) + string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[1].range) + } else { + string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[0].range) + } + value = string + } else { + let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSender(senderName!, recipientName, dateString) : strings.Gift_Unique_OriginalInfo(recipientName, dateString) + let string = NSMutableAttributedString(string: format.string, font: textFont, textColor: textColor) + if let _ = senderPeerId { + string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[0].range) + string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[1].range) + } else { + string.addAttribute(.foregroundColor, value: linkColor, range: format.ranges[0].range) + } + + value = string + } + + content.append(AnyComponentWithIdentity( + id: "info", + component: AnyComponent( + AlertTextComponent(content: .attributed(value), style: .background(.small)) + ) + )) } } - return controller + + let alertController = ChatMessagePaymentAlertController( + context: context, + presentationData: presentationData, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: strings.Gift_RemoveDetails_Action(" $ \(presentationStringsFormattedNumber(Int32(clamping: removeInfoStars), presentationData.dateTimeFormat.groupingSeparator))").string, type: .default, action: { + commit() + }), + .init(title: strings.Common_Cancel) + ], + navigationController: navigationController, + chatPeerId: context.account.peerId, + showBalance: removeInfoStars > 0 + ) + return alertController } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index aed94639bc..f625cc31be 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -15,6 +15,9 @@ import MultilineTextComponent import TelegramStringFormatting import AlertComponent import TableComponent +import AvatarComponent +import AlertTransferHeaderComponent +import AlertTableComponent public func giftTransferAlertController( context: AccountContext, @@ -145,7 +148,8 @@ public func giftTransferAlertController( theme: presentationData.theme, peer: peer ) - )) + )), + type: .transfer ) ) )) @@ -168,8 +172,9 @@ public func giftTransferAlertController( ) )) - let alertController = AlertScreen( + let alertController = ChatMessagePaymentAlertController( context: context, + presentationData: presentationData, configuration: AlertScreen.Configuration(actionAlignment: .vertical, dismissOnOutsideTap: true, allowInputInset: false), content: content, actions: [ @@ -177,9 +182,12 @@ public func giftTransferAlertController( commit() }), .init(title: strings.Common_Cancel) - ] + ], + navigationController: navigationController, + chatPeerId: context.account.peerId, + showBalance: transferStars > 0 ) - + var dismissAllTooltipsImpl: (() -> Void)? showAttributeInfoImpl = { [weak alertController] tag, text in dismissAllTooltipsImpl?() diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 25c68e0949..a18bc10a2d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -43,6 +43,7 @@ import InfoParagraphComponent import ChatMessagePaymentAlertController import TableComponent import PeerTableCellComponent +import AvatarComponent private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -6215,77 +6216,6 @@ private final class HeaderButtonComponent: CombinedComponent { } } -final class AvatarComponent: Component { - let context: AccountContext - let theme: PresentationTheme - let peer: EnginePeer - - init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { - self.context = context - self.theme = theme - self.peer = peer - } - - static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.peer != rhs.peer { - return false - } - return true - } - - final class View: UIView { - private let avatarNode: AvatarNode - - private var component: AvatarComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0)) - - super.init(frame: frame) - - self.addSubnode(self.avatarNode) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - 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 - - self.avatarNode.frame = CGRect(origin: .zero, size: availableSize) - self.avatarNode.setPeer( - context: component.context, - theme: component.theme, - peer: component.peer, - synchronousLoad: true - ) - - return availableSize - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - private struct GiftViewConfiguration { public static var defaultValue: GiftViewConfiguration { return GiftViewConfiguration(explorerUrl: "https://tonviewer.com") diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift index 1784432e2a..1b5d68b720 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift @@ -16,293 +16,83 @@ import StarsAvatarComponent import PasswordSetupUI import OwnershipTransferController import PresentationDataUtils +import AlertComponent +import AlertTransferHeaderComponent -private final class GiftWithdrawAlertContentNode: 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 avatarView = ComponentView() - - 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, - gift: StarGift.UniqueGift, - 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.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.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - } - - 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) - - 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) - 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: self.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) - let _ = self.avatarView.update( - transition: .immediate, - component: AnyComponent( - StarsAvatarComponent( - context: self.context, - theme: self.presentationTheme, - peer: .transactionPeer(.fragment), - photo: nil, - media: [], - gift: nil, - backgroundColor: .clear, - size: avatarSize - ) - ), - environment: {}, - containerSize: avatarSize - ) - if let view = self.avatarView.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.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 - - var effectiveActionLayout = TextAlertContentActionLayout.vertical - if !"".isEmpty { - // Silence the warning - effectiveActionLayout = .horizontal - } - 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 resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 24.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 - } -} - -public func giftWithdrawAlertController(context: AccountContext, gift: StarGift.UniqueGift, commit: @escaping () -> Void) -> AlertController { +public func giftWithdrawAlertController( + context: AccountContext, + gift: StarGift.UniqueGift, + commit: @escaping () -> Void +) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings + + 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: "fragment", component: AnyComponent( + StarsAvatarComponent( + context: context, + theme: presentationData.theme, + peer: .transactionPeer(.fragment), + photo: nil, + media: [], + gift: nil, + backgroundColor: .clear, + size: CGSize(width: 60.0, height: 60.0) + ) + )), + type: .transfer + ) + ) + )) - let title = strings.Gift_Withdraw_Title - let text = strings.Gift_Withdraw_Text("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))").string - let buttonText = strings.Gift_Withdraw_Proceed - - var dismissImpl: ((Bool) -> Void)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { - dismissImpl?(true) - commit() - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - })] - - let contentNode = GiftWithdrawAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, title: 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 + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Gift_Withdraw_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Gift_Withdraw_Text("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))").string)) + ) + )) + + let alertController = AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: strings.Gift_Withdraw_Proceed, type: .default, action: { + commit() + }), + .init(title: strings.Common_Cancel) + ] + ) + return alertController } -public func confirmGiftWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, reference: StarGiftReference, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { +public func confirmGiftWithdrawalController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + reference: StarGiftReference, + present: @escaping (ViewController, Any?) -> Void, + completion: @escaping (String) -> Void +) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } var dismissImpl: (() -> Void)? @@ -365,40 +155,50 @@ public func confirmGiftWithdrawalController(context: AccountContext, updatedPres return controller } -public func giftWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, reference: StarGiftReference, initialError: RequestStarGiftWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { +public func giftWithdrawalController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + reference: StarGiftReference, + initialError: RequestStarGiftWithdrawalError, + present: @escaping (ViewController, Any?) -> Void, + completion: @escaping (String) -> Void +) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let theme = AlertControllerTheme(presentationData: presentationData) + let strings = presentationData.strings - var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.Gift_Withdraw_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) + var title: String? = strings.Gift_Withdraw_SecurityCheck + var text = strings.Gift_Withdraw_SecurityRequirements - var text = presentationData.strings.Gift_Withdraw_SecurityRequirements - let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0 - - var actions: [TextAlertAction] = [] + var actions: [AlertScreen.Action] = [ + .init(title: strings.Common_OK, type: .default) + ] switch initialError { case .requestPassword: return confirmGiftWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, reference: reference, present: present, completion: completion) case .twoStepAuthTooFresh, .authSessionTooFresh: text = text + presentationData.strings.Gift_Withdraw_ComeBackLater - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] case .twoStepAuthMissing: - actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.Gift_Withdraw_SetupTwoStepAuth, action: { - let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in - if shouldDismiss { - controller.dismiss() - } - }) - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})] + actions = [ + .init(title: strings.Gift_Withdraw_SetupTwoStepAuth, type: .default, action: { + let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in + if shouldDismiss { + controller.dismiss() + } + }) + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), + .init(title: strings.Common_Cancel) + ] default: title = nil - text = presentationData.strings.Login_UnknownError - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + text = strings.Login_UnknownError } - let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor) - let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - return richTextAlertController(context: context, title: title, text: attributedText, actions: actions) + return AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + title: title, + text: text, + actions: actions + ) } diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD index 7f66e412c3..f1e7a53a96 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD @@ -34,6 +34,8 @@ swift_library( "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/ContextUI", "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertTableComponent", + "//submodules/TelegramUI/Components/Gifts/TableComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/ToastComponent", "//submodules/AvatarNode", diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index d1a721c029..e853aaca78 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -26,6 +26,9 @@ import ContextUI import BalancedTextComponent import AlertComponent import PremiumCoinComponent +import AlertComponent +import AlertTableComponent +import TableComponent private func textForTimeout(value: Int32) -> String { if value < 3600 { @@ -208,30 +211,50 @@ final class AffiliateProgramSetupScreenComponent: Component { } else { durationTitle = environment.strings.AffiliateProgram_DurationLifetime } + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: environment.strings.AffiliateSetup_AlertApply_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(environment.strings.AffiliateSetup_AlertApply_Text)) + ) + )) - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - self.environment?.controller()?.present(tableAlert( - theme: presentationData.theme, - title: environment.strings.AffiliateSetup_AlertApply_Title, - text: environment.strings.AffiliateSetup_AlertApply_Text, - table: TableComponent(theme: environment.theme, items: [ - TableComponent.Item(id: 0, title: environment.strings.AffiliateSetup_AlertApply_SectionCommission, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: commissionTitle, font: Font.regular(17.0), textColor: environment.theme.actionSheet.primaryTextColor)) - ))), - TableComponent.Item(id: 1, title: environment.strings.AffiliateSetup_AlertApply_SectionDuration, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: durationTitle, font: Font.regular(17.0), textColor: environment.theme.actionSheet.primaryTextColor)) - ))) - ]), + let tableItems: [TableComponent.Item] = [ + TableComponent.Item(id: 0, title: environment.strings.AffiliateSetup_AlertApply_SectionCommission, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: commissionTitle, font: Font.regular(15.0), textColor: environment.theme.actionSheet.primaryTextColor)) + ))), + TableComponent.Item(id: 1, title: environment.strings.AffiliateSetup_AlertApply_SectionDuration, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: durationTitle, font: Font.regular(15.0), textColor: environment.theme.actionSheet.primaryTextColor)) + ))) + ] + content.append(AnyComponentWithIdentity( + id: "table", + component: AnyComponent( + AlertTableComponent(items: tableItems) + ) + )) + + let alertController = AlertScreen( + context: component.context, + content: content, actions: [ - ComponentAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), - ComponentAlertAction(type: .defaultAction, title: environment.strings.AffiliateSetup_AlertApply_Action, action: { [weak self] in + .init(title: environment.strings.Common_Cancel), + .init(title: environment.strings.AffiliateSetup_AlertApply_Action, type: .default, action: { [weak self] in guard let self else { return } self.applyProgram() }) ] - ), in: .window(.root)) + ) + self.environment?.controller()?.present(alertController, in: .window(.root)) } private func requestApplyEndProgram() { diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/TableComponent.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/TableComponent.swift deleted file mode 100644 index 762cf5c913..0000000000 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/TableComponent.swift +++ /dev/null @@ -1,334 +0,0 @@ -import Foundation -import UIKit -import Display -import ComponentFlow -import TelegramPresentationData -import MultilineTextComponent -import AlertComponent - -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 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, 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 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 = 5.0 - borderImage = generateImage(CGSize(width: 16.0, height: 16.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: 5, topCapHeight: 5) - 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 TableAlertContentComponent: CombinedComponent { - let theme: PresentationTheme - let title: String - let text: String - let table: TableComponent - - init(theme: PresentationTheme, title: String, text: String, table: TableComponent) { - self.theme = theme - self.title = title - self.text = text - self.table = table - } - - static func ==(lhs: TableAlertContentComponent, rhs: TableAlertContentComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.title != rhs.title { - return false - } - if lhs.text != rhs.text { - return false - } - if lhs.table != rhs.table { - return false - } - return true - } - - public static var body: Body { - let title = Child(MultilineTextComponent.self) - let text = Child(MultilineTextComponent.self) - let table = Child(TableComponent.self) - - return { context in - let title = title.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: context.component.title, font: Font.semibold(16.0), textColor: context.component.theme.actionSheet.primaryTextColor)), - horizontalAlignment: .center - ), - availableSize: CGSize(width: context.availableSize.width, height: 10000.0), - transition: .immediate - ) - let text = text.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: context.component.text, font: Font.regular(13.0), textColor: context.component.theme.actionSheet.primaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.2 - ), - availableSize: CGSize(width: context.availableSize.width, height: 10000.0), - transition: .immediate - ) - let table = table.update( - component: context.component.table, - availableSize: CGSize(width: context.availableSize.width, height: 10000.0), - transition: .immediate - ) - - var size = CGSize(width: 0.0, height: 0.0) - - size.width = max(size.width, title.size.width) - size.width = max(size.width, text.size.width) - size.width = max(size.width, table.size.width) - - size.height += title.size.height - size.height += 5.0 - size.height += text.size.height - size.height += 14.0 - size.height += table.size.height - size.height -= 3.0 - - var contentHeight: CGFloat = 0.0 - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - title.size.width) * 0.5), y: contentHeight), size: title.size) - contentHeight += title.size.height + 5.0 - let textFrame = CGRect(origin: CGPoint(x: floor((size.width - text.size.width) * 0.5), y: contentHeight), size: text.size) - contentHeight += text.size.height + 14.0 - let tableFrame = CGRect(origin: CGPoint(x: floor((size.width - table.size.width) * 0.5), y: contentHeight), size: table.size) - contentHeight += table.size.height - - context.add(title - .position(titleFrame.center) - ) - context.add(text - .position(textFrame.center) - ) - context.add(table - .position(tableFrame.center) - ) - - return size - } - } -} - -func tableAlert(theme: PresentationTheme, title: String, text: String, table: TableComponent, actions: [ComponentAlertAction]) -> ViewController { - return componentAlertController( - theme: AlertControllerTheme(presentationTheme: theme, fontSize: .regular), - content: AnyComponent(TableAlertContentComponent( - theme: theme, - title: title, - text: text, - table: table - )), - actions: actions, - actionLayout: .horizontal - ) -} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 71742d960b..c442406bd3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -169,6 +169,9 @@ swift_library( "//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AvatarComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PhotoUpdateConfirmationController.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PhotoUpdateConfirmationController.swift index 4e25766949..ce30f6f28a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PhotoUpdateConfirmationController.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PhotoUpdateConfirmationController.swift @@ -9,239 +9,110 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle -import AvatarNode +import ComponentFlow +import AlertComponent +import AlertTransferHeaderComponent +import AvatarComponent -private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let text: String - - private let textNode: ASTextNode - private let avatarNode: AvatarNode - private let arrowNode: ASImageNode - private let iconNode: ASImageNode - - 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, peer: EnginePeer, image: UIImage, text: String, actions: [TextAlertAction]) { - self.strings = strings - self.text = text - - 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.iconNode = ASImageNode() - self.iconNode.clipsToBounds = true - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - self.iconNode.image = image - self.iconNode.cornerRadius = 30.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.textNode) - self.addSubnode(self.avatarNode) - self.addSubnode(self.arrowNode) - self.addSubnode(self.iconNode) - - 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.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .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) - } - - if let _ = self.iconNode.image { - let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) - transition.updateFrame(node: self.iconNode, frame: iconFrame) - origin.y += avatarSize.height + 10.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)) - - 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 + 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 photoUpdateConfirmationController(context: AccountContext, peer: EnginePeer, image: UIImage, text: String, doneTitle: String, isDark: Bool = true, commit: @escaping () -> Void) -> AlertController { - let theme = defaultDarkColorPresentationTheme - var presentationData = context.sharedContext.currentPresentationData.with { $0 } - if isDark { - presentationData = presentationData.withUpdated(theme: theme) - } +func photoUpdateConfirmationController( + context: AccountContext, + peer: EnginePeer, + image: UIImage, + text: String, + doneTitle: String, + isDark: Bool = true, + commit: @escaping () -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertTransferHeaderComponent( + fromComponent: AnyComponentWithIdentity(id: "user", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: peer + ) + )), + toComponent: AnyComponentWithIdentity(id: "image", component: AnyComponent( + Image( + image: image, + size: CGSize(width: 60.0, height: 60.0), + cornerRadius: 30.0 + ) + )), + type: .transfer + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) - var dismissImpl: ((Bool) -> Void)? - var contentNode: PhotoUpdateConfirmationAlertContentNode? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: doneTitle, action: { - dismissImpl?(true) - commit() - })] - - contentNode = PhotoUpdateConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peer: peer, image: image, 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() - } + var updatedPresentationData: (PresentationData, Signal) + if isDark { + updatedPresentationData = (presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), .single(presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))) + } else { + updatedPresentationData = (presentationData, context.sharedContext.presentationData) } - return controller + + let alertController = AlertScreen( + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: doneTitle, type: .default, action: { + commit() + }) + ], + updatedPresentationData: updatedPresentationData + ) + return alertController } + +//private final class RoundImageComponent: Component { +// let image: UIImage +// +// public init( +// image: UIImage +// ) { +// self.image = image +// } +// +// public static func ==(lhs: RoundImageComponent, rhs: RoundImageComponent) -> Bool { +// if lhs.image !== rhs.image { +// return false +// } +// return true +// } +// +// public final class View: UIImageView { +// private var component: RoundImageComponent? +// private weak var state: EmptyComponentState? +// +// func update(component: RoundImageComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { +// self.component = component +// self.state = state +// +// self.clipsToBounds = true +// self.image = component.image +// self.layer.cornerRadius = 30.0 +// +// return CGSize(width: 60.0, height: 60.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/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index c1409f7b9d..4847c57b14 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -233,7 +233,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return } - let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Title, titleFont: .bold, subtitle: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Text, value: "", placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in + let promptController = promptController(context: self.context, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Title, titleFont: .bold, subtitle: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Text, value: "", placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in guard let self, let value else { return } @@ -304,7 +304,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return } - let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_RenameCollection_Title, titleFont: .bold, value: collection.title, placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in + let promptController = promptController(context: self.context, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_RenameCollection_Title, titleFont: .bold, value: collection.title, placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in guard let self, let value else { return } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index d6810166f0..83ae650acc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -5013,7 +5013,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private func presentAddStoryFolder(addItems: [EngineStoryItem] = []) { let promptController = promptController( - sharedContext: self.context.sharedContext, + context: self.context, updatedPresentationData: nil, text: self.presentationData.strings.Stories_CreateAlbum_Title, titleFont: .bold, @@ -5046,7 +5046,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private func presentRenameStoryFolder(id: Int64, title: String) { let promptController = promptController( - sharedContext: self.context.sharedContext, + context: self.context, updatedPresentationData: nil, text: self.presentationData.strings.Stories_EditAlbum_Title, titleFont: .bold, diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD index 86c4687698..9801faf1c3 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/Markdown", "//submodules/ActivityIndicator", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift index e68b26f827..cece5ef44f 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift @@ -13,6 +13,7 @@ import PresentationDataUtils import PasswordSetupUI import Markdown import OldChannelsController +import AlertComponent private final class ChannelOwnershipTransferPasswordFieldNode: ASDisplayNode, UITextFieldDelegate { private var theme: PresentationTheme @@ -534,7 +535,6 @@ private func commitChannelOwnershipTransferController(context: AccountContext, u private func confirmChannelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, member: TelegramUser, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (EnginePeer.Id?) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let theme = AlertControllerTheme(presentationData: presentationData) var isGroup = true if case let .channel(channel) = peer, case .broadcast = channel.info { @@ -551,69 +551,73 @@ private func confirmChannelOwnershipTransferController(context: AccountContext, text = presentationData.strings.Channel_OwnershipTransfer_DescriptionInfo(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), EnginePeer.user(member).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } - let attributedTitle = NSAttributedString(string: title, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: theme.primaryColor, paragraphAlignment: .center) - let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor) - let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_OwnershipTransfer_ChangeOwner, action: { - present(commitChannelOwnershipTransferController(context: context, peer: peer, member: member, present: present, completion: completion), nil) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { - })], actionLayout: .vertical) + let controller = textAlertController( + context: context, + updatedPresentationData: updatedPresentationData, + title: title, + text: text, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_OwnershipTransfer_ChangeOwner, action: { + present(commitChannelOwnershipTransferController(context: context, peer: peer, member: member, present: present, completion: completion), nil) + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ) return controller } public func channelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, member: TelegramUser, initialError: ChannelOwnershipTransferError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (EnginePeer.Id?) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let theme = AlertControllerTheme(presentationData: presentationData) + let strings = presentationData.strings - var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) - - var text = presentationData.strings.OwnershipTransfer_SecurityRequirements - let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0 + var title: String? = strings.OwnershipTransfer_SecurityCheck + var text = strings.OwnershipTransfer_SecurityRequirements var isGroup = true if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } - var actions: [TextAlertAction] = [] + var actions: [AlertScreen.Action] = [ + .init(title: strings.Common_OK, type: .default) + ] switch initialError { case .requestPassword: return confirmChannelOwnershipTransferController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, member: member, present: present, completion: completion) case .twoStepAuthTooFresh, .authSessionTooFresh: - text = text + presentationData.strings.OwnershipTransfer_ComeBackLater - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + text = text + strings.OwnershipTransfer_ComeBackLater case .twoStepAuthMissing: - actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.OwnershipTransfer_SetupTwoStepAuth, action: { - let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in - if shouldDismiss { - controller.dismiss() - } - }) - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})] + actions = [ + .init(title: strings.OwnershipTransfer_SetupTwoStepAuth, type: .default, action: { + let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in + if shouldDismiss { + controller.dismiss() + } + }) + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), + .init(title: strings.Common_Cancel) + ] case .adminsTooMuch: title = nil - text = isGroup ? presentationData.strings.Group_OwnershipTransfer_ErrorAdminsTooMuch : presentationData.strings.Channel_OwnershipTransfer_ErrorAdminsTooMuch - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + text = isGroup ? strings.Group_OwnershipTransfer_ErrorAdminsTooMuch : strings.Channel_OwnershipTransfer_ErrorAdminsTooMuch case .userPublicChannelsTooMuch: title = nil - text = presentationData.strings.Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + text = strings.Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch case .userBlocked, .restricted: title = nil - text = isGroup ? presentationData.strings.Group_OwnershipTransfer_ErrorPrivacyRestricted : presentationData.strings.Channel_OwnershipTransfer_ErrorPrivacyRestricted - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + text = isGroup ? strings.Group_OwnershipTransfer_ErrorPrivacyRestricted : strings.Channel_OwnershipTransfer_ErrorPrivacyRestricted default: title = nil - text = presentationData.strings.Login_UnknownError - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + text = strings.Login_UnknownError } - let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor) - let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - return richTextAlertController(context: context, title: title, text: attributedText, actions: actions) + return AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + title: title, + text: text, + actions: actions + ) } diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift index 4d5f6e9edc..e94aaa5200 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift @@ -12,6 +12,7 @@ import AlertUI import PresentationDataUtils import PasswordSetupUI import Markdown +import AlertComponent private func commitOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -80,41 +81,44 @@ private func commitOwnershipTransferController(context: AccountContext, updatedP public func ownershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, initialError: MessageActionCallbackError, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let theme = AlertControllerTheme(presentationData: presentationData) + let strings = presentationData.strings - var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) + var title: String? = strings.OwnershipTransfer_SecurityCheck + var text = strings.OwnershipTransfer_SecurityRequirements - var text = presentationData.strings.OwnershipTransfer_SecurityRequirements - var actions: [TextAlertAction] = [] - let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0 + var actions: [AlertScreen.Action] = [ + .init(title: strings.Common_OK, type: .default) + ] switch initialError { case .requestPassword: return commitOwnershipTransferController(context: context, updatedPresentationData: updatedPresentationData, present: present, commit: commit, completion: completion) case .twoStepAuthTooFresh, .authSessionTooFresh: text = text + presentationData.strings.OwnershipTransfer_ComeBackLater - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] case .twoStepAuthMissing: - actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.OwnershipTransfer_SetupTwoStepAuth, action: { - let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in - if shouldDismiss { - controller.dismiss() - } - }) - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})] + actions = [ + .init(title: strings.OwnershipTransfer_SetupTwoStepAuth, type: .default, action: { + let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in + if shouldDismiss { + controller.dismiss() + } + }) + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), + .init(title: strings.Common_Cancel) + ] case .userBlocked, .restricted: title = nil text = presentationData.strings.Group_OwnershipTransfer_ErrorPrivacyRestricted - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] default: title = nil text = presentationData.strings.Login_UnknownError - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] } - let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor) - let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - return richTextAlertController(context: context, title: title, text: attributedText, actions: actions) + return AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + title: title, + text: text, + actions: actions + ) } diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 3b2f299274..a677e4a73c 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -700,7 +700,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } - let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { [weak self] link in + let controller = chatTextLinkEditController(context: context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link = link { strongSelf.updateChatPresentationInterfaceState(animated: true, { state in diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift index ba88ee22dc..02ee5142e8 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift @@ -179,14 +179,21 @@ final class PasskeysScreenComponent: Component { guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { return } - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: environment.strings.Passkeys_DeleteAlert_Title, text: environment.strings.Passkeys_DeleteAlert_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { - }), TextAlertAction(type: .destructiveAction, title: environment.strings.Passkeys_DeleteAlert_Action, action: { [weak self] in - guard let self else { - return - } - self.deletePasskey(id: id) - })]), in: .window(.root)) + let alertController = textAlertController( + context: component.context, + title: environment.strings.Passkeys_DeleteAlert_Title, + text: environment.strings.Passkeys_DeleteAlert_Text, + actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: environment.strings.Passkeys_DeleteAlert_Action, action: { [weak self] in + guard let self else { + return + } + self.deletePasskey(id: id) + }) + ] + ) + controller.present(alertController, in: .window(.root)) } private func deletePasskey(id: String) { diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index 22d93e0917..540b78a928 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -291,20 +291,26 @@ final class ChannelAppearanceScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertTitle, text: presentationData.strings.Channel_Appearance_UnsavedChangesAlertText, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertDiscard, action: { [weak self] in - guard let self else { - return - } - self.environment?.controller()?.dismiss() - }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertApply, action: { [weak self] in - guard let self else { - return - } - self.applySettings() - }) - ]), in: .window(.root)) + let alertController = textAlertController( + context: component.context, + title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertTitle, + text: presentationData.strings.Channel_Appearance_UnsavedChangesAlertText, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertDiscard, action: { [weak self] in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertApply, action: { [weak self] in + guard let self else { + return + } + self.applySettings() + }) + ] + ) + self.environment?.controller()?.present(alertController, in: .window(.root)) return false } @@ -534,7 +540,15 @@ final class ChannelAppearanceScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + let alertController = textAlertController( + context: component.context, + title: nil, + text: presentationData.strings.Login_UnknownError, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ] + ) + self.environment?.controller()?.present(alertController, in: .window(.root)) self.isApplyingSettings = false self.state?.updated(transition: .immediate) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index f3918590f6..ec99bc566b 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -298,21 +298,28 @@ final class UserAppearanceScreenComponent: Component { if !resolvedState.changes.isEmpty { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertTitle, text: presentationData.strings.Channel_Appearance_UnsavedChangesAlertText, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertDiscard, action: { [weak self] in - guard let self else { - return - } - self.environment?.controller()?.dismiss() - }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertApply, action: { [weak self] in - guard let self else { - return - } - self.applySettings() - }) - ]), in: .window(.root)) + let alertController = textAlertController( + context: component.context, + title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertTitle, + text: presentationData.strings.Channel_Appearance_UnsavedChangesAlertText, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertDiscard, action: { [weak self] in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertApply, action: { [weak self] in + guard let self else { + return + } + self.applySettings() + }) + ] + ) + self.environment?.controller()?.present(alertController, in: .window(.root)) + return false } @@ -800,7 +807,16 @@ final class UserAppearanceScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + let alertController = textAlertController( + context: component.context, + title: nil, + text: presentationData.strings.Login_UnknownError, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ] + ) + self.environment?.controller()?.present(alertController, in: .window(.root)) self.isApplyingSettings = false self.state?.updated(transition: .immediate) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index b9da307db8..44e173dfaa 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -992,7 +992,7 @@ final class ShareWithPeersScreenComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) let promptController = promptController( - sharedContext: component.context.sharedContext, + context: component.context, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), text: presentationData.strings.Stories_CreateAlbum_Title, titleFont: .bold, diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD index 4e7000f275..496eaecb38 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD @@ -42,6 +42,7 @@ swift_library( "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift index cc62907d39..4a5015d20f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift @@ -8,6 +8,7 @@ import AccountContext import PasswordSetupUI import Markdown import OwnershipTransferController +import AlertComponent public func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, amount: Int64, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -74,38 +75,41 @@ public func confirmStarsRevenueWithdrawalController(context: AccountContext, upd public func starsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let theme = AlertControllerTheme(presentationData: presentationData) + let strings = presentationData.strings - var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) - - var text = presentationData.strings.Monetization_Withdraw_SecurityRequirements - let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0 - - var actions: [TextAlertAction] = [] + var title: String? = strings.OwnershipTransfer_SecurityCheck + var text = strings.Monetization_Withdraw_SecurityRequirements + + var actions: [AlertScreen.Action] = [ + .init(title: strings.Common_OK, type: .default) + ] switch initialError { - case .requestPassword: + case .requestPassword: return confirmStarsRevenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, amount: amount, present: present, completion: completion) - case .twoStepAuthTooFresh, .authSessionTooFresh: - text = text + presentationData.strings.Monetization_Withdraw_ComeBackLater - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] - case .twoStepAuthMissing: - actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.OwnershipTransfer_SetupTwoStepAuth, action: { + case .twoStepAuthTooFresh, .authSessionTooFresh: + text = text + strings.Monetization_Withdraw_ComeBackLater + case .twoStepAuthMissing: + actions = [ + .init(title: strings.OwnershipTransfer_SetupTwoStepAuth, type: .default, action: { let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in if shouldDismiss { controller.dismiss() } }) present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})] - default: - title = nil - text = presentationData.strings.Login_UnknownError - actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + }), + .init(title: strings.Common_Cancel) + ] + default: + title = nil + text = strings.Login_UnknownError } - let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor) - let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - return richTextAlertController(context: context, title: title, text: attributedText, actions: actions) + return AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + title: title, + text: text, + actions: actions + ) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 57c21f083c..a037a504ca 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -7575,7 +7575,7 @@ public final class StoryItemSetContainerComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) let promptController = promptController( - sharedContext: component.context.sharedContext, + context: component.context, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), text: presentationData.strings.Stories_CreateAlbum_Title, titleFont: .bold, diff --git a/submodules/TelegramUI/Components/SuggestedPostApproveAlert/Sources/SuggestedPostApproveAlert.swift b/submodules/TelegramUI/Components/SuggestedPostApproveAlert/Sources/SuggestedPostApproveAlert.swift index 8a39ab8130..8afac2bf80 100644 --- a/submodules/TelegramUI/Components/SuggestedPostApproveAlert/Sources/SuggestedPostApproveAlert.swift +++ b/submodules/TelegramUI/Components/SuggestedPostApproveAlert/Sources/SuggestedPostApproveAlert.swift @@ -410,7 +410,18 @@ private final class SuggestedPostAlertImpl: AlertController { } } -public func SuggestedPostApproveAlert(presentationData: PresentationData, 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, toastText: String?) -> AlertController { +public func SuggestedPostApproveAlert( + presentationData: PresentationData, + 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, + toastText: String? +) -> AlertController { let theme = AlertControllerTheme(presentationData: presentationData) var dismissImpl: (() -> Void)? diff --git a/submodules/TelegramUI/Components/TextFieldComponent/BUILD b/submodules/TelegramUI/Components/TextFieldComponent/BUILD index 4d16bc5b8e..5a8eba3a89 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/BUILD +++ b/submodules/TelegramUI/Components/TextFieldComponent/BUILD @@ -18,7 +18,6 @@ swift_library( "//submodules/AccountContext", "//submodules/InvisibleInkDustNode", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", - "//submodules/ChatTextLinkEditUI", "//submodules/Pasteboard", "//submodules/ImageTransparency", "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 3b76429a5f..ac25591b55 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -10,7 +10,6 @@ import EmojiTextAttachmentView import AccountContext import TextFormat import Pasteboard -import ChatTextLinkEditUI import MobileCoreServices import ImageTransparency import ChatInputTextNode @@ -161,6 +160,9 @@ public final class TextFieldComponent: Component { public let externalHandlingForMultilinePaste: Bool public let formatMenuAvailability: FormatMenuAvailability public let returnKeyType: UIReturnKeyType + public let keyboardType: UIKeyboardType + public let autocapitalizationType: UITextAutocapitalizationType + public let autocorrectionType: UITextAutocorrectionType public let lockedFormatAction: () -> Void public let present: (ViewController) -> Void public let paste: (PasteData) -> Void @@ -188,6 +190,9 @@ public final class TextFieldComponent: Component { externalHandlingForMultilinePaste: Bool = false, formatMenuAvailability: FormatMenuAvailability, returnKeyType: UIReturnKeyType = .default, + keyboardType: UIKeyboardType = .default, + autocapitalizationType: UITextAutocapitalizationType = .sentences, + autocorrectionType: UITextAutocorrectionType = .default, lockedFormatAction: @escaping () -> Void, present: @escaping (ViewController) -> Void, paste: @escaping (PasteData) -> Void, @@ -214,11 +219,14 @@ public final class TextFieldComponent: Component { self.externalHandlingForMultilinePaste = externalHandlingForMultilinePaste self.formatMenuAvailability = formatMenuAvailability self.returnKeyType = returnKeyType + self.keyboardType = keyboardType self.lockedFormatAction = lockedFormatAction self.present = present self.paste = paste self.returnKeyAction = returnKeyAction self.backspaceKeyAction = backspaceKeyAction + self.autocapitalizationType = autocapitalizationType + self.autocorrectionType = autocorrectionType } public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool { @@ -282,6 +290,15 @@ public final class TextFieldComponent: Component { if lhs.returnKeyType != rhs.returnKeyType { return false } + if lhs.keyboardType != rhs.keyboardType { + return false + } + if lhs.autocapitalizationType != rhs.autocapitalizationType { + return false + } + if lhs.autocorrectionType != rhs.autocorrectionType { + return false + } return true } @@ -409,7 +426,7 @@ public final class TextFieldComponent: Component { let inputState = f(self.inputState) let currentAttributedText = self.textView.attributedText - let updatedAttributedText = textAttributedStringForStateText(context: component.context, stateText: inputState.inputText, fontSize: component.fontSize, textColor: component.textColor, accentTextColor: component.accentColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + let updatedAttributedText = textAttributedStringForStateText(context: component.context, stateText: inputState.inputText, fontSize: component.fontSize, textColor: component.textColor, accentTextColor: component.accentColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) }) if currentAttributedText != updatedAttributedText { @@ -986,7 +1003,7 @@ public final class TextFieldComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: component.theme) let updatedPresentationData: (initial: PresentationData, signal: Signal) = (presentationData, .single(presentationData)) - let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, updatedPresentationData: updatedPresentationData, account: component.context.account, text: text.string, link: link, allowEmpty: true, apply: { [weak self] link in + let controller = component.context.sharedContext.makeLinkEditController(context: component.context, updatedPresentationData: updatedPresentationData, text: text.string, link: link, apply: { [weak self] link in if let self { if let link { if !link.isEmpty { @@ -1388,6 +1405,15 @@ public final class TextFieldComponent: Component { if self.textView.returnKeyType != component.returnKeyType { self.textView.returnKeyType = component.returnKeyType } + if self.textView.keyboardType != component.keyboardType { + self.textView.keyboardType = component.keyboardType + } + if self.textView.autocapitalizationType != component.autocapitalizationType { + self.textView.autocapitalizationType = component.autocapitalizationType + } + if self.textView.autocorrectionType != component.autocorrectionType { + self.textView.autocorrectionType = component.autocorrectionType + } if let initialText = component.externalState.initialText { component.externalState.initialText = nil diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/CollapseIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/CollapseIcon.imageset/Contents.json new file mode 100644 index 0000000000..d6b25de386 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/CollapseIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "summary.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/CollapseIcon.imageset/summary.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/CollapseIcon.imageset/summary.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f50bced82ee8c8dd4c73e3a8c8dd8b1226dc385c GIT binary patch literal 4636 zcmZu#c_3767bldaq(w?nx3no{=FW0wrbYJ1(vYMgjWIG}1~ZdMgedh&3)w|TB=S

Oac!+E7 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 LnANNGcNqK+?wi`= literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpandIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpandIcon.imageset/Contents.json new file mode 100644 index 0000000000..13001193ec --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpandIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "original.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpandIcon.imageset/original.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpandIcon.imageset/original.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9f7203cc1aba47e54348718b897a59ea6eabc00d GIT binary patch literal 4641 zcmZu#c|4Tc8z+?Bk`^gRy`@bt``RLVWN9o(MH*vdG-jA3lO;r{D=lOfAz31qO7_ZB zxRj-owTKoeWeG|7z3&*Kd;85FGspLNp6|1s^Et5NSX^#-P!ycIiPP=z}z3z%uBU35gBpG`2HcEHR_Aj__PT44yy~ zBNiYQT4`$oTpou`bq53qccK`SB`wO8Y_E|pEotff73HVRqondrYh67Vz)V>jE=8TY zaPfVIMV5h!3=L%Mb!Ho`^x)Y!FI;5VK3h)C#{T4d;2#p{FV)StGOKwX&6{rQ%YAh( ztHj^$V?x{e#2?9%QWse=1}ILN{G*h^j(fZfoR!o@W#?80kv$7l7@e=>{DfXXLu{|* z`6Z$4v6Tux7VvBys*Wfx-d{FJ`BA1Mn3#{qPgqvbZr9T-pQIC`th%CBuTV{DJNef( zWxvW&rB#Yj6?@nDADOV%LL1x0Ynm(qYa={?q=-NdLdAHV3BxIP3*EE9_ZLGY^n1H( zaLJ+#M|O`Ht637F)%H8>*VfRB5}GfmCaF;8ux8DvUeg3JwO>yr$g5OIxo2m)PYM$! zk5)f*P8_~43rw1)Go*FX;4*o&ehq86j)BFw+0O%%>t!AXNHYUuJZAUbR~-w|SqQ9s z;C5ebE+}_KN4Z(5;N*6S)JVY3t1DMZz`*+#BOcc=dQ ztD*Jy48>x_Fr6j!j`bUt%SBLEWY1Nth(awH36SZe2$rr?Oc(Zb+6##Gv-b(pJ6*=s z`pT>dNc(CkSRqGQcNy3{=aCNDQqu<93@i;d_v`ouzvysVCXbLJ03QyT!xw^p) zx72(Ww*IN>&S3zz?8^C9!3cxs)IC?@8#+F0`><$OX?)A+lMVM{s0k>hne2vD5vQt! zmz~_u<}o!f(zVOqsVRr&1$hV#O{BVT2EAJU;rG>(g)7%Gg zZ%8CGLeXoX7vLrR@`9?+?|#JBR)K1%qBekh_`cJ0VB>(=*og`5#C+Yr!#W;oWfu#A z?*y;aQ%DZ;2t^sitl7Lc>z*Giph_++Erw5-0VBJEArMwTFpQY zp=NItzUEs^S@^~()heH=+$hJ5j+^~$e??7Po2C1J5}0=1viTY%*~5ZrOg(TZ%Qa_t zMofxbhQMkd?oHuhi)u>s!L#X?Z}7}|!y7j(HEc@F&#?bUa8vk(;h#I5rqa-S+u(ro z=Zy!D?z{J!wAhl($Ont`zL)0P$fY@DRM?u>YFLp|F&Tr5z<0AQ$W6$VDC0CgIDS3E zPtGdY?1*=~W69U~7O}$N$w4g>_uK=at?K;S#hK%rZ<^nPAETQ#E^AcYitItuItpC6 zWEAX#HtKe?vd8GK_!ZCM4#btmG2-UnkqisBOxL2L>)j$<`L5=!)^{B4$hEF-U3S~C zKC*Y!u(P*HS-f^yYI1kFdP8PoZ&PdozR@8$KD905hw+7s%IBfawlwrL#y7rv;h1$# z_uiq&KE7 zB)ob}MBDYhGp{gCGEVhsVVi4TnH@e>m0h=z*6)hIn}(qj2Js`c~Vefd9}z8ps8v;R)=sP{VKnZaA`)EJ{%tM_j2u5+pB-7b~K zDlb@!SU9C)E&R7)6Cx7!Y`uUa9H@1)za@BX`0`Y@)!ViFw35e#tN4+{y@j6bSsjVR zeMLj~>yVg&2F69xru8E-WPnS;J{94B{YzISq|Z`oj(cUffVK0&n4&XI<1XGb$f zV+ZdHIecmB4H=mCwXKi%@P#wCh5t#zf6ipsuMX*+fQtcx0qLe@(PyG=Yn_T7M|RvD z?!pAoE9hE*;6=4Un_X(lvz}gIDBl=y9$pq14r<)ORg? zyk*^%;))(&ituB_s}P@4%R)@mHPs15?PqK*C>azOb){6KxTRj-=bhVrkE7X&*oyS_ zh-}FEwD+a$ja-XEekZ~vRy>Wng^r2&8WVv|A_pe8J>_X`E5IE4tLDwB&hnn3q1}Qd zzROOaCyytX+IzMON=+xq$Ii;$4Q>oJkv63u1Xn&p3)T(zSsiX1xYpA1wdb*t?{2ey z$jRX-ljbW~k#SdAqg|q+qBod(?S7r4y&hQDmB^s&jdt8Z)ACb!MUORJUd z@$#kbp=uQ-5Jcnc;|G$mrow`O*3P{5GEyyF7uzz=!g* zA91E1)_-5m9?fm>&v{-jTqXTRx>}mi5}yAeXnfA-$_e{KiZ-Qde}i47WzmhnpKD$v z{}T3xsGfISr9m3(&FK74w)u6|J2&CSMp`p1wlc9PxM_HzY9jBZZCSz9ks8k~&)&10 zOFH-q`23A{3*{!^xA5_^)(z>b$)0c4M`cozvn-iYdI%R{Vr{! zqIll3D~u}<%es;0kLC2PC|C0AWsMH@zp9ar&OJ0x#L28oeP6LR`o`y%2{*5lJS~02 z5&kk7GB1fYzt{5f#q*=D+k9)fZ2R0R+uZ#^e{3D^7)QPuDCWLJ{OAu0q|5jPZJGT= z!BElU%RqnF>pkU@HMVc|%N{5kKU*DPlvy@Bapq;!lk_!vLUt9U zBK_;eeh%67WB<9;_`bES@3H&iwvE0USoiF{JAIgU=CV^tH)LmCs$hGMhp3hU^OX63j2 z6voCq2=rqk+Z3H1jJPy>&QbgPS%onFXz{Y0=haP=pV6P5kBm#$yr@CHkz}uZMK?M& zax#9;*&XsaHOHiER{Mso0pk2?ee%h=ok?>3`@EJHIUhP&dDqSjFICHsqWEBB2e@ck zk4-;I$f%RwJ$)BBo?+t6?$<4r@4obN-tk#^;9lcp^1c45&nC-|qlcbf{ zhsC?+7%xl$uirZ5RBl{#iT}_fahuwu?8u@Dl|~m`W%bb@%wh#yL*bj~v7#2%mrga7=y#Xl3|1C0O62{FsV2VNn$iWlCGoxaE|}&2NQ4w(D>;H(1hs#pw>eE zhy*ka1J0not^S`>!b1ao(?m@-lL>Q@$xzeDAR4Lz0Zk&HKnTM@ZqRrbLqS!;LMuE1 zJ=Gux!%lSw;>03wQ>{c}5UA;1LK@;>Ax~!qIfBgKnZko41Q~_FWAGq(YVNRNgg}50 zaWy0h8X!_9pfF-}NNb9KC<+-Y(8X*{v&=a zg#5+??VsP6Y#DSfI)}ocyU-!maFB2@3>aJwCiS=hn`Xn{G3g+W!>7X>Q;tknE^Lt+ zg~N8{)1dzvY%*dTai?lb$wf-=7jnvY6Pv?0!eCLE|B_4C%&4p*;3hgtEEScUD=C49 zM$`&@Dv!!!ANhAO0w{0*+CwN{suA!vNWeoA2f8RoFlcXy#2{-L!w{hn{{w?|$sZU2 zI$hw;Z?>dA`LOU#{*NBCmH+U8L*f7MfkR`Uo=p4Tai|O?oddvaFfo9dya_r9>`_Dv x7DK{n{O2!G>ycJ$HV=f`DjFlCHN%?@R|Kjsmq+FBL^Z-;u{dD;dc!?N{{!ow+VlVb literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index c5164ccba6..8953143641 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2426,11 +2426,21 @@ private func extractAccountManagerState(records: AccountRecordsView 1 { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.BusinessLink_AlertTextLimitText, actions: [ - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) - ]), in: .window(.root)) - + let alertController = textAlertController( + context: strongSelf.context, + title: nil, + text: strongSelf.presentationData.strings.BusinessLink_AlertTextLimitText, + actions: [ + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) + ] + ) + strongSelf.present(alertController, in: .window(.root)) return } @@ -1886,8 +1899,9 @@ extension ChatControllerImpl { titleString = self.presentationData.strings.Chat_DeletePaidMessageTon_Title textString = self.presentationData.strings.Chat_DeletePaidMessageTon_Text } - self.present(standardTextAlertController( - theme: AlertControllerTheme(presentationData: self.presentationData), + + let alertController = textAlertController( + context: self.context, title: titleString, text: textString, actions: [ @@ -1899,9 +1913,9 @@ extension ChatControllerImpl { }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}) ], - actionLayout: .vertical, - parseMarkdown: true - ), in: .window(.root)) + actionLayout: .vertical + ) + self.present(alertController, in: .window(.root)) } if let contextController { contextController.dismiss(completion: commit) @@ -3653,7 +3667,7 @@ extension ChatControllerImpl { } } - let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: strongSelf.updatedPresentationData, account: strongSelf.context.account, text: text?.string ?? "", link: link, allowEmpty: true, apply: { [weak self] link in + let controller = chatTextLinkEditController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, text: text?.string ?? "", link: link, apply: { [weak self] link in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link { if !link.isEmpty { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift index c6306556ac..7ea7d6cb91 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift @@ -280,13 +280,19 @@ extension ChatControllerImpl { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - }), - TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { - beginClear(.scheduledMessages) - }) - ], parseMarkdown: true), in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, + text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { + beginClear(.scheduledMessages) + }) + ] + ) + strongSelf.present(alertController, in: .window(.root)) })) } else { if let _ = canClearForMyself ?? canClearForEveryone { @@ -310,13 +316,19 @@ extension ChatControllerImpl { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: confirmationText, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - }), - TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { - beginClear(.forEveryone) - }) - ], parseMarkdown: true), in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, + text: confirmationText, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { + beginClear(.forEveryone) + }) + ] + ) + strongSelf.present(alertController, in: .window(.root)) })) } if let canClearForMyself = canClearForMyself { @@ -330,13 +342,19 @@ extension ChatControllerImpl { items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if mainPeer.id == context.account.peerId, let strongSelf = self { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - }), - TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { - beginClear(.forLocalPeer) - }) - ], parseMarkdown: true), in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, + text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { + beginClear(.forLocalPeer) + }) + ] + ) + strongSelf.present(alertController, in: .window(.root)) } else { beginClear(.forLocalPeer) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index ff3181762d..d4e1caea88 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -17,6 +17,7 @@ import PremiumUI import TooltipUI import TopMessageReactions import TelegramNotices +import PresentationDataUtils extension ChatControllerImpl { func openMessageContextMenu(message: Message, selectAll: Bool, node: ASDisplayNode, frame: CGRect, anyRecognizer: UIGestureRecognizer?, location: CGPoint?) -> Void { @@ -433,9 +434,15 @@ extension ChatControllerImpl { if case let .known(reactionSettings) = reactionSettings, let starsAllowed = reactionSettings.starsAllowed, !starsAllowed { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_ToastStarsReactionsDisabled(peer.debugDisplayTitle).string, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) - ]), in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: nil, + text: strongSelf.presentationData.strings.Chat_ToastStarsReactionsDisabled(peer.debugDisplayTitle).string, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) + ] + ) + strongSelf.present(alertController, in: .window(.root)) } return } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageFactCheck.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageFactCheck.swift index 481e550d2b..ca5ac6c4eb 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageFactCheck.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageFactCheck.swift @@ -16,7 +16,12 @@ extension ChatControllerImpl { break } } - let controller = factCheckAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, value: currentText, entities: currentEntities, apply: { [weak self] text, entities in + let controller = factCheckAlertController( + context: self.context, + updatedPresentationData: self.updatedPresentationData, + value: currentText, + entities: currentEntities, + apply: { [weak self] text, entities in guard let self else { return } diff --git a/submodules/TelegramUI/Sources/ChatAgeRestrictionAlertController.swift b/submodules/TelegramUI/Sources/ChatAgeRestrictionAlertController.swift deleted file mode 100644 index 0cc95eb7d8..0000000000 --- a/submodules/TelegramUI/Sources/ChatAgeRestrictionAlertController.swift +++ /dev/null @@ -1,302 +0,0 @@ -import Foundation -import UIKit -import SwiftSignalKit -import AsyncDisplayKit -import Display -import Postbox -import TelegramCore -import TelegramPresentationData -import TelegramUIPreferences -import AccountContext -import AppBundle -import AvatarNode -import CheckNode -import Markdown - -private let textFont = Font.regular(13.0) -private let boldTextFont = Font.semibold(13.0) - -private func formattedText(_ text: String, color: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { - return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: color), linkAttribute: { _ in return nil}), textAlignment: textAlignment) -} - -private final class ChatAgeRestrictionAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let title: String - private let text: String - - private let titleNode: ImmediateTextNode - private let textNode: ASTextNode - - private let alwaysCheckNode: InteractiveCheckNode - private let alwaysLabelNode: ASTextNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - var alwaysShow: Bool = false { - didSet { - self.alwaysCheckNode.setSelected(self.alwaysShow, animated: true) - } - } - - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, actions: [TextAlertAction]) { - self.strings = strings - self.title = title - self.text = text - - self.titleNode = ImmediateTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.textAlignment = .center - - self.textNode = ASTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.maximumNumberOfLines = 0 - self.textNode.lineSpacing = 0.1 - - self.alwaysCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) - self.alwaysLabelNode = ASTextNode() - self.alwaysLabelNode.maximumNumberOfLines = 2 - self.alwaysLabelNode.isUserInteractionEnabled = true - - 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.alwaysCheckNode) - self.addSubnode(self.alwaysLabelNode) - - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.alwaysCheckNode.valueChanged = { [weak self] value in - if let strongSelf = self { - strongSelf.alwaysShow = !strongSelf.alwaysShow - } - } - - self.updateTheme(theme) - } - - override func didLoad() { - super.didLoad() - - self.alwaysLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.alwaysTap(_:)))) - } - - @objc private func alwaysTap(_ gestureRecognizer: UITapGestureRecognizer) { - if self.alwaysCheckNode.isUserInteractionEnabled { - self.alwaysShow = !self.alwaysShow - } - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - self.alwaysLabelNode.attributedText = formattedText(self.strings.SensitiveContent_ShowAlways, color: theme.primaryColor) - - 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: 17.0) - - let titleSize = self.titleNode.updateLayout(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 + 6.0 - - var entriesHeight: CGFloat = 0.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 - - if self.alwaysLabelNode.supernode != nil { - origin.y += 21.0 - entriesHeight += 21.0 - - let checkSize = CGSize(width: 22.0, height: 22.0) - let condensedSize = CGSize(width: size.width - 76.0, height: size.height) - - let alwaysSize = self.alwaysLabelNode.measure(condensedSize) - let totalWidth = checkSize.width + alwaysSize.width + 12.0 - let originX = floorToScreenPixels((size.width - totalWidth) / 2.0) - transition.updateFrame(node: self.alwaysLabelNode, frame: CGRect(origin: CGPoint(x: originX + checkSize.width + 12.0, y: origin.y), size: alwaysSize)) - transition.updateFrame(node: self.alwaysCheckNode, frame: CGRect(origin: CGPoint(x: originX, y: origin.y - 2.0), size: checkSize)) - origin.y += alwaysSize.height - entriesHeight += alwaysSize.height - } - - 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.vertical - 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: titleSize.height + textSize.height + entriesHeight + actionsHeight + 8.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 - } -} - -public func chatAgeRestrictionAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, completion: @escaping (Bool) -> Void) -> AlertController { - let theme = defaultDarkColorPresentationTheme - let presentationData: PresentationData - if let updatedPresentationData { - presentationData = updatedPresentationData.initial - } else { - presentationData = context.sharedContext.currentPresentationData.with { $0 } - } - let strings = presentationData.strings - - var dismissImpl: ((Bool) -> Void)? - var getContentNodeImpl: (() -> ChatAgeRestrictionAlertContentNode?)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: strings.SensitiveContent_ViewAnyway, action: { - if let alwaysShow = getContentNodeImpl?()?.alwaysShow { - completion(alwaysShow) - } else { - completion(false) - } - dismissImpl?(true) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - })] - - let title = strings.SensitiveContent_Title - let text = strings.SensitiveContent_Text - - let contentNode = ChatAgeRestrictionAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, actions: actions) - getContentNodeImpl = { [weak contentNode] in - return contentNode - } - - 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/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 3f6a3fa9dd..042700bbe7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -146,6 +146,7 @@ import ChatTextInputPanelNode import ChatInputAccessoryPanel import GlobalControlPanelsContext import ChatSearchNavigationContentNode +import ChatAgeRestrictionAlertController public final class ChatControllerOverlayPresentationData { public let expandData: (ASDisplayNode?, () -> Void) @@ -776,12 +777,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G textString = strongSelf.presentationData.strings.QuickReply_ChatRemoveAwayMessage_Text } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: titleString, text: textString, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.QuickReply_ChatRemoveGeneric_DeleteAction, action: { [weak strongSelf] in - strongSelf?.dismiss() - }) - ]), in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: titleString, + text: textString, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.QuickReply_ChatRemoveGeneric_DeleteAction, action: { [weak strongSelf] in + strongSelf?.dismiss() + }) + ] + ) + strongSelf.present(alertController, in: .window(.root)) return false } @@ -793,12 +800,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let message = inputText.string if message != link.message || entities != link.entities { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Business_Links_AlertUnsavedText, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), - TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Business_Links_AlertUnsavedAction, action: { [weak strongSelf] in - strongSelf?.dismiss() - }) - ]), in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: nil, + text: strongSelf.presentationData.strings.Business_Links_AlertUnsavedText, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Business_Links_AlertUnsavedAction, action: { [weak strongSelf] in + strongSelf?.dismiss() + }) + ] + ) + strongSelf.present(alertController, in: .window(.root)) return false } @@ -1809,9 +1822,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .known(reactionSettings) = reactionSettings, let starsAllowed = reactionSettings.starsAllowed, !starsAllowed { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_ToastStarsReactionsDisabled(peer.debugDisplayTitle).string, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) - ]), in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: nil, + text: strongSelf.presentationData.strings.Chat_ToastStarsReactionsDisabled(peer.debugDisplayTitle).string, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) + ] + ) + strongSelf.present(alertController, in: .window(.root)) } return } @@ -2454,7 +2473,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if message.effectivelyIncoming(strongSelf.context.account.peerId) { switch buttonType { case 0: - let promptController = promptController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: strongSelf.updatedPresentationData, text: strongSelf.presentationData.strings.Chat_PostSuggestion_Reject_Title, titleFont: .bold, value: "", placeholder: strongSelf.presentationData.strings.Chat_PostSuggestion_Reject_Placeholder, characterLimit: 4096, apply: { value in + let promptController = promptController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, text: strongSelf.presentationData.strings.Chat_PostSuggestion_Reject_Title, titleFont: .bold, value: "", placeholder: strongSelf.presentationData.strings.Chat_PostSuggestion_Reject_Placeholder, characterLimit: 4096, apply: { value in guard let self else { return } @@ -2478,13 +2497,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let configuration = StarsSubscriptionConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) funds = (amount, amount.currency == .stars ? Int(configuration.channelMessageSuggestionStarsCommissionPermille) : Int(configuration.channelMessageSuggestionTonCommissionPermille)) } - - #if DEBUG - if "".isEmpty { - funds = nil - } - #endif - + var isAdmin = false if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = strongSelf.presentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { isAdmin = true @@ -7160,24 +7173,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G controller.navigationPresentation = .modal controller.setState(.custom(icon: .animation("BroadcastGroup"), title: presentationData.strings.BroadcastGroups_IntroTitle, subtitle: nil, text: presentationData.strings.BroadcastGroups_IntroText, buttonTitle: presentationData.strings.BroadcastGroups_Convert, secondaryButtonTitle: presentationData.strings.BroadcastGroups_Cancel, footerText: nil), animated: false) controller.proceed = { [weak controller] result in - let attributedTitle = NSAttributedString(string: presentationData.strings.BroadcastGroups_ConfirmationAlert_Title, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) - let attributedText = parseMarkdownIntoAttributedString(presentationData.strings.BroadcastGroups_ConfirmationAlert_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - let alertController = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone() - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Convert, action: { [weak controller] in - controller?.dismiss() - - let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone() - - let _ = (convertGroupToGigagroup(account: context.account, peerId: peerId) - |> deliverOnMainQueue).startStandalone(completed: { - let participantsLimit = context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .gigagroupConversion(text: presentationData.strings.BroadcastGroups_Success(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.decimalSeparator)).string), elevatedLayout: false, action: { _ in return false }), in: .current) - }) - })]) + let alertController = textAlertController( + context: context, + title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Title, + text: presentationData.strings.BroadcastGroups_ConfirmationAlert_Text, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone() + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Convert, action: { [weak controller] in + controller?.dismiss() + + let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone() + + let _ = (convertGroupToGigagroup(account: context.account, peerId: peerId) + |> deliverOnMainQueue).startStandalone(completed: { + let participantsLimit = context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .gigagroupConversion(text: presentationData.strings.BroadcastGroups_Success(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.decimalSeparator)).string), elevatedLayout: false, action: { _ in return false }), in: .current) + }) + }) + ] + ) controller?.present(alertController, in: .window(.root)) } strongSelf.push(controller) @@ -8387,9 +8403,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject, let messageLimit = customChatContents.messageLimit { if let originalHistoryView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, originalHistoryView.entries.count + mappedMessages.count > messageLimit { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_QuickReplyMediaMessageLimitReachedText(Int32(messageLimit)), actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) - ]), in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: nil, + text: strongSelf.presentationData.strings.Chat_QuickReplyMediaMessageLimitReachedText(Int32(messageLimit)), + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) + ] + ) + strongSelf.present(alertController, in: .window(.root)) return } } diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index e289e8722a..a8834e5866 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -404,8 +404,8 @@ extension ChatControllerImpl { titleString = self.presentationData.strings.Chat_DeletePaidMessageTon_Title textString = self.presentationData.strings.Chat_DeletePaidMessageTon_Text } - self.present(standardTextAlertController( - theme: AlertControllerTheme(presentationData: self.presentationData), + self.present(textAlertController( + context: self.context, title: titleString, text: textString, actions: [ @@ -450,8 +450,8 @@ extension ChatControllerImpl { deleteOneAction = self.presentationData.strings.ScheduledMessages_DeleteRepeatingActionSingle deleteAllAction = self.presentationData.strings.ScheduledMessages_DeleteRepeatingActionMultiple } - self.present(standardTextAlertController( - theme: AlertControllerTheme(presentationData: self.presentationData), + self.present(textAlertController( + context: self.context, title: title, text: text, actions: [ diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 55216c5f81..c262c102e8 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -59,6 +59,7 @@ import AdPanelHeaderPanelComponent import MessageFeeHeaderPanelComponent import LegacyChatHeaderPanelComponent import ChatSearchNavigationContentNode +import PresentationDataUtils final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -4336,11 +4337,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - if !found { + if !found, let controller = self.controller { let authorName: String = (replyMessage.author.flatMap(EnginePeer.init))?.compactDisplayTitle ?? "" let errorTextData = self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedText(authorName) let errorText = errorTextData.string - self.controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.context.sharedContext.currentPresentationData.with({ $0 })), title: self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedTitle, text: errorText, actions: [ + controller.present(textAlertController(context: self.context, title: self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedTitle, text: errorText, actions: [ TextAlertAction(type: .genericAction, title: self.chatPresentationInterfaceState.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedActionEdit, action: { [weak self] in guard let self, let controller = self.controller else { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 551db3f3fb..5ea627c2dc 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -1013,14 +1013,14 @@ extension ChatControllerImpl { } else { text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentCantSendMultipleFiles: { guard let strongSelf = self else { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentJpegConversionAlert: { completion in - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { completion(false) }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: { completion(true) @@ -1463,7 +1463,7 @@ extension ChatControllerImpl { text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentSchedulePicker: { [weak self] media, done in if let strongSelf = self { strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in @@ -1588,19 +1588,19 @@ extension ChatControllerImpl { switch itemType { case .image: if bannedSendPhotos != nil { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } case .video: if bannedSendVideos != nil { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } case .gif: if bannedSendGifs != nil { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 9640bc72eb..bd1b5f7ee7 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -20,6 +20,7 @@ import ChatMessageItemCommon import ChatMessageItemView import ReactionSelectionNode import AnimatedTextComponent +import PresentationDataUtils extension ChatControllerImpl { func presentTagPremiumPaywall() { @@ -392,7 +393,7 @@ extension ChatControllerImpl { if case let .known(reactionSettings) = reactionSettings, let starsAllowed = reactionSettings.starsAllowed, !starsAllowed { if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_ToastStarsReactionsDisabled(peer.debugDisplayTitle).string, actions: [ + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_ToastStarsReactionsDisabled(peer.debugDisplayTitle).string, actions: [ TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {}) ]), in: .window(.root)) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 81195abb78..0384225266 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1178,8 +1178,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState c?.dismiss(completion: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - controllerInteraction.presentController(standardTextAlertController( - theme: AlertControllerTheme(presentationData: presentationData), + controllerInteraction.presentController(textAlertController( + context: context, title: presentationData.strings.Chat_ScheduledForceSendProcessingVideo_Title, text: presentationData.strings.Chat_ScheduledForceSendProcessingVideo_Text, actions: [ diff --git a/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift b/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift index 4d055b9b80..7221f5dbd0 100644 --- a/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift +++ b/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift @@ -95,10 +95,7 @@ final class NotificationContainerControllerNode: ASDisplayNode { }) } - var useCompactLayout = false - if let validLayout = self.validLayout { - useCompactLayout = min(validLayout.size.width, validLayout.size.height) < 375.0 - } + let useCompactLayout = "".isEmpty let itemNode = item.node(compact: useCompactLayout) let containerNode = NotificationItemContainerNode(theme: self.presentationData.theme, contentNode: itemNode) @@ -165,10 +162,7 @@ final class NotificationContainerControllerNode: ASDisplayNode { } self.topItemAndNode = nil - var useCompactLayout = false - if let validLayout = self.validLayout { - useCompactLayout = min(validLayout.size.width, validLayout.size.height) < 375.0 - } + let useCompactLayout = "".isEmpty let itemNode = item.node(compact: useCompactLayout) let containerNode = NotificationItemContainerNode(theme: self.presentationData.theme, contentNode: itemNode) diff --git a/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift b/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift index aff92e020c..44ff16bdec 100644 --- a/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift +++ b/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift @@ -2,14 +2,16 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import ComponentFlow import TelegramPresentationData import ChatMessageNotificationItem +import GlassBackgroundComponent final class NotificationItemContainerNode: ASDisplayNode { - private let backgroundNode: ASImageNode - - private var validLayout: ContainerViewLayout? + private let theme: PresentationTheme + private let backgroundView = GlassBackgroundView() + var item: NotificationItem? private var hapticFeedback: HapticFeedback? @@ -40,6 +42,8 @@ final class NotificationItemContainerNode: ASDisplayNode { } } + private var validLayout: ContainerViewLayout? + var dismissed: ((NotificationItem) -> Void)? var cancelTimeout: ((NotificationItem) -> Void)? var resumeTimeout: ((NotificationItem) -> Void)? @@ -48,15 +52,10 @@ final class NotificationItemContainerNode: ASDisplayNode { init(theme: PresentationTheme, contentNode: NotificationItemNode?) { self.contentNode = contentNode - - self.backgroundNode = ASImageNode() - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.image = PresentationResourcesRootController.inAppNotificationBackground(theme) - + self.theme = theme + super.init() - self.addSubnode(self.backgroundNode) if let contentNode { self.addSubnode(contentNode) } @@ -65,6 +64,8 @@ final class NotificationItemContainerNode: ASDisplayNode { override func didLoad() { super.didLoad() + self.view.insertSubview(self.backgroundView, at: 0) + if let contentNode = self.contentNode, !contentNode.acceptsTouches { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) @@ -76,13 +77,13 @@ final class NotificationItemContainerNode: ASDisplayNode { func animateIn() { if let _ = self.validLayout { - self.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.backgroundNode.bounds.size.height), to: CGPoint(), duration: 0.4, additive: true) + self.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.backgroundView.bounds.size.height), to: CGPoint(), duration: 0.4, additive: true) } } func animateOut(completion: @escaping () -> Void) { if let _ = self.validLayout { - self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -self.backgroundNode.bounds.size.height), duration: 0.4, removeOnCompletion: false, additive: true, completion: { _ in + self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -self.backgroundView.bounds.size.height), duration: 0.4, removeOnCompletion: false, additive: true, completion: { _ in completion() }) } else { @@ -100,7 +101,7 @@ final class NotificationItemContainerNode: ASDisplayNode { if let statusBarHeight = layout.statusBarHeight, statusBarHeight >= 39.0 { if layout.deviceMetrics.hasDynamicIsland { - contentInsets.top = statusBarHeight + contentInsets.top = statusBarHeight + 6.0 } else if statusBarHeight >= 44.0 { contentInsets.top += 34.0 } else { @@ -113,7 +114,10 @@ final class NotificationItemContainerNode: ASDisplayNode { let contentWidth = containerWidth - contentInsets.left - contentInsets.right let contentHeight = contentNode.updateLayout(width: contentWidth, transition: transition) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - containerWidth - 8.0 * 2.0) / 2.0), y: contentInsets.top - 16.0), size: CGSize(width: containerWidth + 8.0 * 2.0, height: 8.0 + contentHeight + 20.0 + 8.0))) + let backgroundInset: CGFloat = 8.0 + let backgroundSize = CGSize(width: containerWidth - backgroundInset * 2.0, height: contentHeight) + self.backgroundView.update(size: backgroundSize, cornerRadius: 24.0, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: self.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), transition: ComponentTransition(transition)) + transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - backgroundSize.width) / 2.0), y: contentInsets.top), size: backgroundSize)) transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - contentWidth) / 2.0), y: contentInsets.top), size: CGSize(width: contentWidth, height: contentHeight))) } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index f80fc2a201..461a48b7cd 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -96,7 +96,7 @@ func openResolvedUrlImpl( present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } case .inaccessiblePeer: - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Conversation_ErrorInaccessibleMessage, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Conversation_ErrorInaccessibleMessage, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) case let .botStart(peer, payload): openPeer(EnginePeer(peer), .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive))) case let .groupBotStart(botPeerId, payload, adminRights, peerType): @@ -133,9 +133,8 @@ func openResolvedUrlImpl( let addMemberImpl = { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let theme = AlertControllerTheme(presentationData: presentationData) - let attributedTitle = NSAttributedString(string: presentationData.strings.Bot_AddToChat_Add_MemberAlertTitle, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: theme.primaryColor, paragraphAlignment: .center) - + let strings = presentationData.strings + var isGroup: Bool = false var peerTitle: String = "" if case let .legacyGroup(peer) = peer { @@ -148,51 +147,54 @@ func openResolvedUrlImpl( peerTitle = peer.title } - let text = isGroup ? presentationData.strings.Bot_AddToChat_Add_MemberAlertTextGroup(peerTitle).string : presentationData.strings.Bot_AddToChat_Add_MemberAlertTextChannel(peerTitle).string + let text = isGroup ? strings.Bot_AddToChat_Add_MemberAlertTextGroup(peerTitle).string : strings.Bot_AddToChat_Add_MemberAlertTextChannel(peerTitle).string - let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor) - let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Bot_AddToChat_Add_MemberAlertAdd, action: { - if payload.isEmpty { - if peerId.namespace == Namespaces.Peer.CloudGroup { - let _ = (context.engine.peers.addGroupMember(peerId: peerId, memberId: botPeerId) - |> deliverOnMainQueue).startStandalone(completed: { - controller?.dismiss() - }) - } else { - let _ = (context.engine.peers.addChannelMember(peerId: peerId, memberId: botPeerId) - |> deliverOnMainQueue).startStandalone(completed: { - controller?.dismiss() - }) - } - } else { - let _ = (context.engine.messages.requestStartBotInGroup(botPeerId: botPeerId, groupPeerId: peerId, payload: payload) - |> deliverOnMainQueue).startStandalone(next: { result in - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - guard let peer = peer else { - return + let alertController = textAlertController( + context: context, + title: strings.Bot_AddToChat_Add_MemberAlertTitle, + text: text, + actions: [ + TextAlertAction(type: .defaultAction, title: strings.Bot_AddToChat_Add_MemberAlertAdd, action: { + if payload.isEmpty { + if peerId.namespace == Namespaces.Peer.CloudGroup { + let _ = (context.engine.peers.addGroupMember(peerId: peerId, memberId: botPeerId) + |> deliverOnMainQueue).startStandalone(completed: { + controller?.dismiss() + }) + } else { + let _ = (context.engine.peers.addChannelMember(peerId: peerId, memberId: botPeerId) + |> deliverOnMainQueue).startStandalone(completed: { + controller?.dismiss() + }) } - if let navigationController = navigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) - } - switch result { - case let .channelParticipant(participant): - context.peerChannelMemberCategoriesContextsManager.externallyAdded(peerId: peerId, participant: participant) - case .none: - break - } - controller?.dismiss() - }) - }, error: { _ in - - }) - } - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - })], actionLayout: .vertical) - present(controller, nil) + } else { + let _ = (context.engine.messages.requestStartBotInGroup(botPeerId: botPeerId, groupPeerId: peerId, payload: payload) + |> deliverOnMainQueue).startStandalone(next: { result in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let peer = peer else { + return + } + if let navigationController = navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + } + switch result { + case let .channelParticipant(participant): + context.peerChannelMemberCategoriesContextsManager.externallyAdded(peerId: peerId, participant: participant) + case .none: + break + } + controller?.dismiss() + }) + }, error: { _ in + + }) + } + }), + TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {}) + ] + ) + present(alertController, nil) } if case let .channel(peer) = peer { diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 136c5b944a..bc32dcc759 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -561,9 +561,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } if isToken { - context.sharedContext.presentGlobalController(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.AuthSessions_AddDevice_UrlLoginHint, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { - }), + context.sharedContext.presentGlobalController(textAlertController(context: context, title: nil, text: presentationData.strings.AuthSessions_AddDevice_UrlLoginHint, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}), ], parseMarkdown: true), nil) return } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 8523d853db..e6d81edfdc 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -92,6 +92,7 @@ import AttachmentFileController import NewContactScreen import PasskeysScreen import GiftDemoScreen +import ChatTextLinkEditUI private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -4023,6 +4024,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return SendInviteLinkScreen(context: context, subject: subject, peers: peers, theme: theme) } + public func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController { + return chatTextLinkEditController(context: context, updatedPresentationData: updatedPresentationData, text: text, link: link, apply: apply) + } + @available(iOS 13.0, *) public func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController { return await PostSuggestionsSettingsScreen(context: context, peerId: peerId, completion: {}) diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index e0764819f6..23c3251494 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -58,6 +58,9 @@ swift_library( "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AvatarComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppAddToAttachmentController.swift b/submodules/WebUI/Sources/WebAppAddToAttachmentController.swift new file mode 100644 index 0000000000..5c96fd4112 --- /dev/null +++ b/submodules/WebUI/Sources/WebAppAddToAttachmentController.swift @@ -0,0 +1,174 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import PhotoResources +import ComponentFlow +import AlertComponent +import AlertCheckComponent +import BundleIconComponent + +public func addWebAppToAttachmentController(context: AccountContext, peerName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], requestWriteAccess: Bool, completion: @escaping (Bool) -> Void) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let checkState = AlertCheckComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertWebAppAttachmentHeaderComponent(context: context, icons: icons) + ) + )) + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.WebApp_AddToAttachmentTitle) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.WebApp_AddToAttachmentText(peerName).string)) + ) + )) + if requestWriteAccess { + content.append(AnyComponentWithIdentity( + id: "check", + component: AnyComponent( + AlertCheckComponent(title: strings.WebApp_AddToAttachmentAllowMessages(peerName).string, initialValue: false, externalState: checkState) + ) + )) + } + + let alertController = AlertScreen( + context: context, + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.WebApp_AddToAttachmentAdd, type: .default, action: { + completion(requestWriteAccess && checkState.value) + }) + ] + ) + return alertController +} + +private final class AlertWebAppAttachmentHeaderComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + let context: AccountContext + let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] + + public init( + context: AccountContext, + icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] + ) { + self.context = context + self.icons = icons + } + + public static func ==(lhs: AlertWebAppAttachmentHeaderComponent, rhs: AlertWebAppAttachmentHeaderComponent) -> Bool { + return true + } + + public final class View: UIView { + private let appIcon = ComponentView() + private let icon = ComponentView() + + private var appIconImage: UIImage? + private var appIconDisposable: Disposable? + + private var component: AlertWebAppAttachmentHeaderComponent? + private weak var state: EmptyComponentState? + + deinit { + self.appIconDisposable?.dispose() + } + + func update(component: AlertWebAppAttachmentHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + var peerIcon: TelegramMediaFile? + if let icon = component.icons[.iOSStatic] { + peerIcon = icon + } else if let icon = component.icons[.default] { + peerIcon = icon + } + + if let peerIcon { + let _ = freeMediaFileInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: peerIcon)).start() + self.appIconDisposable = (svgIconImageFile(account: component.context.account, fileReference: .standalone(media: peerIcon)) + |> deliverOnMainQueue).start(next: { [weak self] transform in + if let self { + let availableSize = CGSize(width: 48.0, height: 48.0) + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: availableSize, boundingSize: availableSize, intrinsicInsets: UIEdgeInsets()) + let drawingContext = transform(arguments) + self.appIconImage = drawingContext?.generateImage()?.withRenderingMode(.alwaysTemplate) + + self.state?.updated() + } + }) + } + } + + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let appIconSize = CGSize(width: 42.0, height: 42.0) + let _ = self.appIcon.update( + transition: .immediate, + component: AnyComponent( + Image(image: self.appIconImage, tintColor: environment.theme.actionSheet.controlAccentColor) + ), + environment: {}, + containerSize: appIconSize + ) + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + BundleIconComponent(name: "Chat/Attach Menu/BotPlus", tintColor: environment.theme.actionSheet.controlAccentColor) + ), + environment: {}, + containerSize: availableSize + ) + + let totalWidth: CGFloat = 42.0 + iconSize.width + + let appIconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0) - 2.0, y: 3.0), size: appIconSize) + if let imageView = self.appIcon.view { + if imageView.superview == nil { + self.addSubview(imageView) + } + imageView.frame = appIconFrame + } + + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0) + appIconSize.width, y: 0.0), size: iconSize) + if let imageView = self.icon.view { + if imageView.superview == nil { + self.addSubview(imageView) + } + imageView.frame = iconFrame + } + + return CGSize(width: availableSize.width, height: appIconSize.height + 17.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/WebUI/Sources/WebAppAlertContentNode.swift b/submodules/WebUI/Sources/WebAppAlertContentNode.swift deleted file mode 100644 index 76c1633354..0000000000 --- a/submodules/WebUI/Sources/WebAppAlertContentNode.swift +++ /dev/null @@ -1,340 +0,0 @@ -import Foundation -import UIKit -import SwiftSignalKit -import AsyncDisplayKit -import Display -import Postbox -import TelegramCore -import TelegramPresentationData -import TelegramUIPreferences -import AccountContext -import AppBundle -import PhotoResources -import CheckNode -import Markdown - -private let textFont = Font.regular(13.0) -private let boldTextFont = Font.semibold(13.0) - -private func formattedText(_ text: String, color: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { - return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: color), linkAttribute: { _ in return nil}), textAlignment: textAlignment) -} - -private final class WebAppAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let peerName: String - private let peerIcon: TelegramMediaFile? - - private let textNode: ASTextNode - private let appIconNode: ASImageNode - private let iconNode: ASImageNode - - private let allowWriteCheckNode: InteractiveCheckNode - private let allowWriteLabelNode: ASTextNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - private var iconDisposable: Disposable? - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - var allowWriteAccess: Bool = true { - didSet { - self.allowWriteCheckNode.setSelected(self.allowWriteAccess, animated: true) - } - } - - init(account: Account, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peerName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], requestWriteAccess: Bool, actions: [TextAlertAction]) { - self.strings = strings - self.peerName = peerName - - if let icon = icons[.iOSStatic] { - self.peerIcon = icon - } else if let icon = icons[.default] { - self.peerIcon = icon - } else { - self.peerIcon = nil - } - - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 0 - - self.appIconNode = ASImageNode() - self.appIconNode.displaysAsynchronously = false - self.appIconNode.displayWithoutProcessing = true - - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - - self.allowWriteCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) - self.allowWriteCheckNode.setSelected(true, animated: false) - self.allowWriteLabelNode = ASTextNode() - self.allowWriteLabelNode.maximumNumberOfLines = 4 - self.allowWriteLabelNode.isUserInteractionEnabled = true - - 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.textNode) - self.addSubnode(self.appIconNode) - self.addSubnode(self.iconNode) - - if requestWriteAccess { - self.addSubnode(self.allowWriteCheckNode) - self.addSubnode(self.allowWriteLabelNode) - } - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.allowWriteCheckNode.valueChanged = { [weak self] value in - if let strongSelf = self { - strongSelf.allowWriteAccess = !strongSelf.allowWriteAccess - } - } - - self.updateTheme(theme) - - if let peerIcon = self.peerIcon { - let _ = freeMediaFileInteractiveFetched(account: account, userLocation: .other, fileReference: .standalone(media: peerIcon)).start() - self.iconDisposable = (svgIconImageFile(account: account, fileReference: .standalone(media: peerIcon)) - |> deliverOnMainQueue).start(next: { [weak self] transform in - if let strongSelf = self { - let availableSize = CGSize(width: 48.0, height: 48.0) - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: availableSize, boundingSize: availableSize, intrinsicInsets: UIEdgeInsets()) - let drawingContext = transform(arguments) - let image = drawingContext?.generateImage()?.withRenderingMode(.alwaysTemplate) - strongSelf.appIconNode.image = generateTintedImage(image: image, color: theme.accentColor, backgroundColor: nil) - } - }) - } - } - - deinit { - self.iconDisposable?.dispose() - } - - override func didLoad() { - super.didLoad() - - self.allowWriteLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.allowWriteTap(_:)))) - } - - @objc private func allowWriteTap(_ gestureRecognizer: UITapGestureRecognizer) { - if self.allowWriteCheckNode.isUserInteractionEnabled { - self.allowWriteAccess = !self.allowWriteAccess - } - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.textNode.attributedText = NSAttributedString(string: strings.WebApp_AddToAttachmentText(self.peerName).string, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - self.appIconNode.image = generateTintedImage(image: self.appIconNode.image, color: theme.accentColor) - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/BotPlus"), color: theme.accentColor) - - self.allowWriteLabelNode.attributedText = formattedText(strings.WebApp_AddToAttachmentAllowMessages(self.peerName).string, color: theme.primaryColor) - - 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) - - var iconSize = CGSize() - var iconFrame = CGRect() - if let icon = self.iconNode.image { - iconSize = icon.size - iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: origin.y), size: iconSize) - origin.y += iconSize.height + 16.0 - } - - let textSize = self.textNode.measure(size) - var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize) - origin.y += textSize.height - - var entriesHeight: CGFloat = 0.0 - - if self.allowWriteLabelNode.supernode != nil { - origin.y += 16.0 - entriesHeight += 16.0 - - let checkSize = CGSize(width: 22.0, height: 22.0) - let condensedSize = CGSize(width: size.width - 76.0, height: size.height) - - let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize) - transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize)) - transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize)) - origin.y += allowWriteSize.height - entriesHeight += allowWriteSize.height - } - - 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) - - var contentWidth = max(textSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - let resultSize = CGSize(width: resultWidth, height: iconSize.height + textSize.height + entriesHeight + actionsHeight + 17.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 - } - - iconFrame.origin.x = floorToScreenPixels((resultSize.width - iconFrame.width) / 2.0) + 21.0 - - transition.updateFrame(node: self.appIconNode, frame: CGRect(x: iconFrame.minX - 50.0, y: iconFrame.minY + 3.0, width: 42.0, height: 42.0)) - transition.updateFrame(node: self.iconNode, frame: iconFrame) - - textFrame.origin.x = floorToScreenPixels((resultSize.width - textFrame.width) / 2.0) - transition.updateFrame(node: self.textNode, frame: textFrame) - - return resultSize - } -} - -public func addWebAppToAttachmentController(context: AccountContext, peerName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], requestWriteAccess: Bool, completion: @escaping (Bool) -> Void) -> AlertController { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let theme = presentationData.theme - let strings = presentationData.strings - - var dismissImpl: ((Bool) -> Void)? - var getContentNodeImpl: (() -> WebAppAlertContentNode?)? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_AddToAttachmentAdd, action: { - if requestWriteAccess, let allowWriteAccess = getContentNodeImpl?()?.allowWriteAccess { - completion(allowWriteAccess) - } else { - completion(false) - } - dismissImpl?(true) - })] - - let contentNode = WebAppAlertContentNode(account: context.account, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peerName: peerName, icons: icons, requestWriteAccess: requestWriteAccess, actions: actions) - getContentNodeImpl = { [weak contentNode] in - return contentNode - } - - 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/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 80a3b10136..bc04472f13 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -686,7 +686,14 @@ public final class WebAppController: ViewController, AttachmentContainable { if let data = try? Data(contentsOf: url), let pass = try? PKPass(data: data) { let passLibrary = PKPassLibrary() if passLibrary.containsPass(pass) { - let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: self.presentationData.strings.WebBrowser_PassExistsError, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {})]) + let alertController = AlertScreen( + context: self.context, + title: nil, + text: self.presentationData.strings.WebBrowser_PassExistsError, + actions: [ + .init(title: self.presentationData.strings.Common_OK, type: .default) + ] + ) self.controller?.present(alertController, in: .window(.root)) } else if let controller = PKAddPassesViewController(pass: pass) { self.controller?.view.window?.rootViewController?.present(controller, animated: true) @@ -820,24 +827,28 @@ public final class WebAppController: ViewController, AttachmentContainable { func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { var completed = false - let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: self.controller?.updatedPresentationData, text: prompt, value: defaultText, apply: { value in - if !completed { - completed = true - if let value = value { - completionHandler(value) - } else { - completionHandler(nil) + let promptController = promptController( + context: self.context, + updatedPresentationData: self.controller?.updatedPresentationData, + text: prompt, + value: defaultText, + apply: { value in + if !completed { + completed = true + if let value = value { + completionHandler(value) + } else { + completionHandler(nil) + } } - } - }) - promptController.dismissed = { byOutsideTap in - if byOutsideTap { + }, + dismissed: { if !completed { completed = true completionHandler(nil) } } - } + ) self.controller?.present(promptController, in: .window(.root)) } @@ -2366,14 +2377,21 @@ public final class WebAppController: ViewController, AttachmentContainable { alertText = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string } } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: alertTitle, text: alertText, actions: [ - TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_No, action: { - updateAccessGranted(false) - }), - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Yes, action: { - updateAccessGranted(true) - }) - ], parseMarkdown: false), in: .window(.root)) + + let alertController = AlertScreen( + context: self.context, + title: alertTitle, + text: alertText, + actions: [ + .init(title: self.presentationData.strings.Common_No, action: { + updateAccessGranted(false) + }), + .init(title: self.presentationData.strings.Common_Yes, type: .default, action: { + updateAccessGranted(true) + }) + ] + ) + controller.present(alertController, in: .window(.root)) }) } @@ -2812,17 +2830,23 @@ public final class WebAppController: ViewController, AttachmentContainable { } let text: String = self.presentationData.strings.WebApp_Download_Text(controller.botName, fileName, fileSizeString).string - let alertController = standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: title, text: text, actions: [ - TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { [weak self] in - let data: JSON = [ - "status": "cancelled" - ] - self?.webView?.sendEvent(name: "file_download_requested", data: data.string) - }), - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.WebApp_Download_Download, action: { [weak self] in - self?.startDownload(url: url, fileName: fileName, fileSize: fileSize, isMedia: isMedia) - }) - ], parseMarkdown: true) + + let alertController = AlertScreen( + context: self.context, + title: title, + text: text, + actions: [ + .init(title: self.presentationData.strings.Common_Cancel, action: { [weak self] in + let data: JSON = [ + "status": "cancelled" + ] + self?.webView?.sendEvent(name: "file_download_requested", data: data.string) + }), + .init(title: self.presentationData.strings.WebApp_Download_Download, type: .default, action: { [weak self] in + self?.startDownload(url: url, fileName: fileName, fileSize: fileSize, isMedia: isMedia) + }) + ] + ) alertController.dismissed = { [weak self] byOutsideTap in let data: JSON = [ "status": "cancelled" @@ -3000,7 +3024,7 @@ public final class WebAppController: ViewController, AttachmentContainable { accountPeer: accountPeer, botName: controller.botName, icons: iconStatusEmoji, - completion: { [weak self] result in + completion: { [weak self] result, byOutsideTap in guard let self, let controller = self.controller else { return } @@ -3055,17 +3079,13 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string) } - let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in - return WebAppPermissionsState(location: current?.location, emojiStatus: WebAppPermissionsState.EmojiStatus(isRequested: true)) - }.startStandalone() + if !byOutsideTap { + let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in + return WebAppPermissionsState(location: current?.location, emojiStatus: WebAppPermissionsState.EmojiStatus(isRequested: true)) + }.startStandalone() + } } ) - alertController.dismissed = { [weak self] byOutsideTap in - let data: JSON = [ - "status": "cancelled" - ] - self?.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string) - } controller.present(alertController, in: .window(.root)) }) } @@ -3896,13 +3916,22 @@ public final class WebAppController: ViewController, AttachmentContainable { private func removeAttachBot() { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - self.present(textAlertController(context: context, title: presentationData.strings.WebApp_RemoveConfirmationTitle, text: presentationData.strings.WebApp_RemoveAllConfirmationText(self.botName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { [weak self] in - guard let self else { - return - } - let _ = self.context.engine.messages.removeBotFromAttachMenu(botId: self.botId).start() - self.dismiss() - })], parseMarkdown: true), in: .window(.root)) + let alertController = AlertScreen( + context: self.context, + title: presentationData.strings.WebApp_RemoveConfirmationTitle, + text: presentationData.strings.WebApp_RemoveAllConfirmationText(self.botName).string, + actions: [ + .init(title: presentationData.strings.Common_Cancel), + .init(title: presentationData.strings.Common_OK, type: .default, action: { [weak self] in + guard let self else { + return + } + let _ = self.context.engine.messages.removeBotFromAttachMenu(botId: self.botId).start() + self.dismiss() + }) + ] + ) + self.present(alertController, in: .window(.root)) } override public func loadDisplayNode() { diff --git a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift index 316962fb11..ea8dc4ed73 100644 --- a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift +++ b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift @@ -4,6 +4,7 @@ import SwiftSignalKit import AsyncDisplayKit import Display import Postbox +import ComponentFlow import TelegramCore import TelegramPresentationData import TelegramUIPreferences @@ -13,352 +14,236 @@ import AvatarNode import EmojiTextAttachmentView import TextFormat import Markdown - -private final class IconsNode: ASDisplayNode { - private let context: AccountContext - private var animationLayer: InlineStickerItemLayer? - - private var files: [TelegramMediaFile.Accessor] - private var currentIndex = 0 - private var switchingToNext = false - - private var timer: SwiftSignalKit.Timer? - - private var currentParams: (size: CGSize, theme: PresentationTheme)? - - init(context: AccountContext, files: [TelegramMediaFile.Accessor]) { - self.context = context - self.files = files - - super.init() - } - - deinit { - self.timer?.invalidate() - } - - func updateLayout(size: CGSize, theme: PresentationTheme) { - self.currentParams = (size, theme) - - if self.timer == nil { - self.timer = SwiftSignalKit.Timer(timeout: 2.5, repeat: true, completion: { [weak self] in - guard let self else { - return - } - self.switchingToNext = true - if let (size, theme) = self.currentParams { - self.updateLayout(size: size, theme: theme) - } - }, queue: Queue.mainQueue()) - self.timer?.start() - } - - let animationLayer: InlineStickerItemLayer - var disappearingAnimationLayer: InlineStickerItemLayer? - if let current = self.animationLayer, !self.switchingToNext { - animationLayer = current - } else { - if self.switchingToNext { - self.currentIndex = (self.currentIndex + 1) % self.files.count - disappearingAnimationLayer = self.animationLayer - self.switchingToNext = false - } - let file = self.files[self.currentIndex]._parse() - let emoji = ChatTextInputTextCustomEmojiAttribute( - interactivelySelectedFromPackId: nil, - fileId: file.fileId.id, - file: file - ) - animationLayer = InlineStickerItemLayer( - context: .account(self.context), - userLocation: .other, - attemptSynchronousLoad: false, - emoji: emoji, - file: file, - cache: self.context.animationCache, - renderer: self.context.animationRenderer, - unique: true, - placeholderColor: theme.list.mediaPlaceholderColor, - pointSize: CGSize(width: 20.0, height: 20.0), - loopCount: 1 - ) - animationLayer.isVisibleForAnimations = true - animationLayer.dynamicColor = theme.actionSheet.controlAccentColor - self.view.layer.addSublayer(animationLayer) - self.animationLayer = animationLayer - - animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - animationLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true) - animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) - } - - animationLayer.frame = CGRect(origin: .zero, size: CGSize(width: 20.0, height: 20.0)) - - if let disappearingAnimationLayer { - disappearingAnimationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - disappearingAnimationLayer.removeFromSuperlayer() - }) - disappearingAnimationLayer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, removeOnCompletion: false, additive: true) - disappearingAnimationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - } - } -} - -private final class WebAppEmojiStatusAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let presentationTheme: PresentationTheme - private let botName: String - - private let textNode: ASTextNode - private let iconBackgroundNode: ASImageNode - private let iconAvatarNode: AvatarNode - private let iconNameNode: ASTextNode - private let iconAnimationNode: IconsNode - - 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, - accountPeer: EnginePeer, - botName: String, - icons: [TelegramMediaFile.Accessor], - actions: [TextAlertAction] - ) { - self.strings = strings - self.presentationTheme = ptheme - self.botName = botName - - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 0 - - self.iconBackgroundNode = ASImageNode() - self.iconBackgroundNode.displaysAsynchronously = false - self.iconBackgroundNode.image = generateStretchableFilledCircleImage(radius: 16.0, color: theme.separatorColor) - - self.iconAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) - self.iconAvatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer) - - self.iconNameNode = ASTextNode() - self.iconNameNode.attributedText = NSAttributedString(string: accountPeer.compactDisplayTitle, font: Font.medium(15.0), textColor: theme.primaryColor) - - self.iconAnimationNode = IconsNode(context: context, files: icons) - - 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.textNode) - self.addSubnode(self.iconBackgroundNode) - self.addSubnode(self.iconAvatarNode) - self.addSubnode(self.iconNameNode) - self.addSubnode(self.iconAnimationNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - } - - override func updateTheme(_ theme: AlertControllerTheme) { - let string = self.strings.WebApp_EmojiPermission_Text(self.botName, self.botName).string - let attributedText = parseMarkdownIntoAttributedString(string, 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.textNode.attributedText = attributedText - - 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 iconSpacing: CGFloat = 6.0 - let iconSize = CGSize(width: 32.0, height: 32.0) - let nameSize = self.iconNameNode.measure(size) - let totalIconWidth = iconSize.width + iconSpacing + nameSize.width + 4.0 + iconSize.width - - let iconBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalIconWidth) / 2.0), y: origin.y), size: CGSize(width: totalIconWidth, height: iconSize.height)) - transition.updateFrame(node: self.iconBackgroundNode, frame: iconBackgroundFrame) - transition.updateFrame(node: self.iconAvatarNode, frame: CGRect(origin: iconBackgroundFrame.origin, size: iconSize).insetBy(dx: 1.0, dy: 1.0)) - transition.updateFrame(node: self.iconNameNode, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + iconSize.width + iconSpacing, y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - nameSize.height) / 2.0)), size: nameSize)) - - self.iconAnimationNode.updateLayout(size: CGSize(width: 20.0, height: 20.0), theme: self.presentationTheme) - self.iconAnimationNode.frame = CGRect(origin: CGPoint(x: iconBackgroundFrame.maxX - iconSize.width - 3.0, y: iconBackgroundFrame.minY), size: iconSize).insetBy(dx: 6.0, dy: 6.0) - - origin.y += iconSize.height + 16.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)) - - 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) - - var contentWidth = minActionsWidth - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - let resultSize = CGSize(width: resultWidth, height: iconSize.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 - } -} +import AlertComponent +import AvatarComponent +import MultilineTextComponent func webAppEmojiStatusAlertController( context: AccountContext, accountPeer: EnginePeer, botName: String, icons: [TelegramMediaFile.Accessor], - completion: @escaping (Bool) -> Void -) -> AlertController { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let theme = presentationData.theme - let strings = presentationData.strings - - var dismissImpl: ((Bool) -> Void)? - var contentNode: WebAppEmojiStatusAlertContentNode? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: strings.WebApp_EmojiPermission_Decline, action: { - dismissImpl?(true) + completion: @escaping (Bool, Bool) -> Void +) -> ViewController { + let strings = context.sharedContext.currentPresentationData.with { $0 }.strings - completion(false) - }), TextAlertAction(type: .defaultAction, title: strings.WebApp_EmojiPermission_Allow, action: { - dismissImpl?(true) - - completion(true) - })] + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "status", + component: AnyComponent( + AlertEmojiStatusComponent(context: context, peer: accountPeer, files: icons) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.WebApp_EmojiPermission_Text(botName, botName).string)) + ) + )) - contentNode = WebAppEmojiStatusAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, accountPeer: accountPeer, botName: botName, icons: icons, actions: actions) - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() + let alertController = AlertScreen( + context: context, + content: content, + actions: [ + .init(title: strings.WebApp_EmojiPermission_Decline, action: { + completion(false, false) + }), + .init(title: strings.WebApp_EmojiPermission_Allow, type: .default, action: { + completion(true, false) + }) + ] + ) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + completion(false, true) } } - return controller + return alertController +} + +private final class AlertEmojiStatusComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + let context: AccountContext + let peer: EnginePeer + let files: [TelegramMediaFile.Accessor] + + public init( + context: AccountContext, + peer: EnginePeer, + files: [TelegramMediaFile.Accessor] + ) { + self.context = context + self.peer = peer + self.files = files + } + + public static func ==(lhs: AlertEmojiStatusComponent, rhs: AlertEmojiStatusComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.files != rhs.files { + return false + } + return true + } + + final class View: UIView { + private let background = ComponentView() + private let title = ComponentView() + private let avatar = ComponentView() + + private var animationLayer: InlineStickerItemLayer? + + private var currentIndex = 0 + private var switchingToNext = false + + private var timer: SwiftSignalKit.Timer? + + private var component: AlertEmojiStatusComponent? + private weak var state: EmptyComponentState? + + func update(component: AlertEmojiStatusComponent, 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.peer.compactDisplayTitle, + font: Font.medium(15.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: availableSize + ) + + let avatarSize = CGSize(width: 30.0, height: 30.0) + let iconSize = CGSize(width: 20.0, height: 20.0) + let avatarMargin: CGFloat = 1.0 + let avatarSpacing: CGFloat = 7.0 + let titleSpacing: CGFloat = 4.0 + let statusMargin: CGFloat = 12.0 + + let backgroundSize = CGSize(width: avatarMargin + avatarSize.width + avatarSpacing + titleSize.width + titleSpacing + iconSize.width + statusMargin, height: 32.0) + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .minEdge, smoothCorners: false)), + environment: {}, + containerSize: backgroundSize + ) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let _ = self.avatar.update( + transition: transition, + component: AnyComponent(AvatarComponent( + context: component.context, + theme: environment.theme, + peer: component.peer + )), + environment: {}, + containerSize: avatarSize + ) + let avatarFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + avatarMargin, y: backgroundFrame.minY + avatarMargin), size: avatarSize) + if let avatarView = self.avatar.view { + if avatarView.superview == nil { + self.addSubview(avatarView) + } + transition.setFrame(view: avatarView, frame: avatarFrame) + } + + let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + avatarMargin + avatarSize.width + avatarSpacing, y: backgroundFrame.minY + floorToScreenPixels((backgroundSize.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + if self.timer == nil { + self.timer = SwiftSignalKit.Timer(timeout: 2.5, repeat: true, completion: { [weak self] in + guard let self else { + return + } + self.switchingToNext = true + self.state?.updated() + }, queue: Queue.mainQueue()) + self.timer?.start() + } + + let animationLayer: InlineStickerItemLayer + var disappearingAnimationLayer: InlineStickerItemLayer? + if let current = self.animationLayer, !self.switchingToNext { + animationLayer = current + } else { + if self.switchingToNext { + self.currentIndex = (self.currentIndex + 1) % component.files.count + disappearingAnimationLayer = self.animationLayer + self.switchingToNext = false + } + let file = component.files[self.currentIndex]._parse() + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: file.fileId.id, + file: file + ) + animationLayer = InlineStickerItemLayer( + context: .account(component.context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: file, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + unique: true, + placeholderColor: environment.theme.list.mediaPlaceholderColor, + pointSize: iconSize, + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + animationLayer.dynamicColor = environment.theme.actionSheet.controlAccentColor + self.layer.addSublayer(animationLayer) + self.animationLayer = animationLayer + + animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animationLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true) + animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + + animationLayer.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - iconSize.width - statusMargin, y: backgroundFrame.minY + floorToScreenPixels((backgroundFrame.height - iconSize.height) / 2.0)), size: iconSize) + + if let disappearingAnimationLayer { + disappearingAnimationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + disappearingAnimationLayer.removeFromSuperlayer() + }) + disappearingAnimationLayer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, removeOnCompletion: false, additive: true) + disappearingAnimationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + + return CGSize(width: availableSize.width, height: backgroundSize.height + 12.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/WebUI/Sources/WebAppLaunchConfirmationController.swift b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift index ba371f9632..2fa118db47 100644 --- a/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift +++ b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift @@ -10,365 +10,13 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle -import AvatarNode -import CheckNode -import Markdown import EmojiStatusComponent - -private let textFont = Font.regular(13.0) -private let boldTextFont = Font.semibold(13.0) - -private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { - return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { _ in return nil}), textAlignment: textAlignment) -} - -private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { - private let context: AccountContext - private let presentationTheme: PresentationTheme - private let strings: PresentationStrings - private let peer: EnginePeer - private let title: String - private let text: String - private let showMore: Bool - - private let titleNode: ImmediateTextNode - private var titleCredibilityIconView: ComponentHostView? - private let textNode: ASTextNode - private let avatarNode: AvatarNode - - private let moreButton: HighlightableButtonNode - private let arrowNode: ASImageNode - - private let allowWriteCheckNode: InteractiveCheckNode - private let allowWriteLabelNode: ASTextNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - private let morePressed: () -> Void - private let termsPressed: () -> Void - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - var allowWriteAccess: Bool = true { - didSet { - self.allowWriteCheckNode.setSelected(self.allowWriteAccess, animated: true) - } - } - - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, title: String, text: String, showMore: Bool, requestWriteAccess: Bool, actions: [TextAlertAction], morePressed: @escaping () -> Void, termsPressed: @escaping () -> Void) { - self.context = context - self.strings = strings - self.presentationTheme = ptheme - self.peer = peer - self.title = title - self.text = text - self.showMore = showMore - self.morePressed = morePressed - self.termsPressed = termsPressed - - self.titleNode = ImmediateTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.textAlignment = .center - - self.textNode = ASTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.maximumNumberOfLines = 0 - - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) - - self.moreButton = HighlightableButtonNode() - - self.arrowNode = ASImageNode() - self.arrowNode.displaysAsynchronously = false - self.arrowNode.displayWithoutProcessing = true - self.arrowNode.isHidden = !showMore - self.arrowNode.contentMode = .scaleAspectFit - - self.allowWriteCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) - self.allowWriteCheckNode.setSelected(true, animated: false) - self.allowWriteLabelNode = ASTextNode() - self.allowWriteLabelNode.maximumNumberOfLines = 4 - self.allowWriteLabelNode.isUserInteractionEnabled = true - - 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.moreButton) - self.moreButton.addSubnode(self.arrowNode) - - if requestWriteAccess { - self.addSubnode(self.allowWriteCheckNode) - self.addSubnode(self.allowWriteLabelNode) - } - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.allowWriteCheckNode.valueChanged = { [weak self] value in - if let strongSelf = self { - strongSelf.allowWriteAccess = !strongSelf.allowWriteAccess - } - } - - self.updateTheme(theme) - - self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer) - - self.moreButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) - } - - override func didLoad() { - super.didLoad() - - self.allowWriteLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.allowWriteTap(_:)))) - - self.textNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.termsTap(_:)))) - } - - @objc private func allowWriteTap(_ gestureRecognizer: UITapGestureRecognizer) { - if self.allowWriteCheckNode.isUserInteractionEnabled { - self.allowWriteAccess = !self.allowWriteAccess - } - } - - @objc private func termsTap(_ gestureRecognizer: UITapGestureRecognizer) { - self.termsPressed() - } - - @objc private func moreButtonPressed() { - self.morePressed() - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = formattedText(self.text, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center) - - self.moreButton.setAttributedTitle(NSAttributedString(string: self.strings.WebApp_LaunchMoreInfo, font: Font.regular(13.0), textColor: theme.accentColor), for: .normal) - self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.accentColor) - - self.allowWriteLabelNode.attributedText = formattedText(strings.WebApp_AddToAttachmentAllowMessages(self.peer.compactDisplayTitle).string, color: theme.primaryColor, linkColor: theme.primaryColor) - - 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), y: origin.y), size: avatarSize) - transition.updateFrame(node: self.avatarNode, frame: avatarFrame) - - origin.y += avatarSize.height + 17.0 - - 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 titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height)) - var totalWidth = titleSize.width - - var statusContent: EmojiStatusComponent.Content? - if self.peer.isScam { - statusContent = .text(color: self.presentationTheme.list.itemDestructiveColor, string: self.strings.Message_ScamAccount.uppercased()) - } else if self.peer.isFake { - statusContent = .text(color: self.presentationTheme.list.itemDestructiveColor, string: self.strings.Message_FakeAccount.uppercased()) - } else if self.peer.isVerified { - statusContent = .verified(fillColor: self.presentationTheme.list.itemCheckColors.fillColor, foregroundColor: self.presentationTheme.list.itemCheckColors.foregroundColor, sizeType: .large) - } - - if let statusContent { - let titleCredibilityIconTransition: ComponentTransition = .immediate - - let titleCredibilityIconView: ComponentHostView - if let current = self.titleCredibilityIconView { - titleCredibilityIconView = current - } else { - titleCredibilityIconView = ComponentHostView() - self.titleCredibilityIconView = titleCredibilityIconView - self.view.addSubview(titleCredibilityIconView) - } - - let titleIconSize = titleCredibilityIconView.update( - transition: titleCredibilityIconTransition, - component: AnyComponent(EmojiStatusComponent( - context: self.context, - animationCache: self.context.animationCache, - animationRenderer: self.context.animationRenderer, - content: statusContent, - isVisibleForAnimations: true, - action: { - } - )), - environment: {}, - containerSize: CGSize(width: 20.0, height: 20.0) - ) - - totalWidth += titleIconSize.width + 2.0 - titleCredibilityIconTransition.setFrame(view: titleCredibilityIconView, frame: CGRect(origin: CGPoint(x:floorToScreenPixels((size.width - totalWidth) / 2.0) + titleSize.width + 2.0, y: origin.y), size: titleIconSize)) - } - - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: origin.y), size: titleSize)) - origin.y += titleSize.height + 6.0 - - var entriesHeight: CGFloat = 0.0 - if self.showMore { - let moreButtonSize = self.moreButton.measure(CGSize(width: size.width - 32.0, height: size.height)) - transition.updateFrame(node: self.moreButton, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - moreButtonSize.width) / 2.0) - 5.0, y: origin.y), size: moreButtonSize)) - transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: moreButtonSize.width + 3.0, y: 4.0), size: CGSize(width: 9.0, height: 9.0))) - origin.y += moreButtonSize.height + 22.0 - entriesHeight += 37.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 - - if self.allowWriteLabelNode.supernode != nil { - origin.y += 16.0 - entriesHeight += 16.0 - - let checkSize = CGSize(width: 22.0, height: 22.0) - let condensedSize = CGSize(width: size.width - 76.0, height: size.height) - - let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize) - transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize)) - transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize)) - origin.y += allowWriteSize.height - entriesHeight += allowWriteSize.height - } - - 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.vertical - 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 + entriesHeight + actionsHeight + 25.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 - } -} +import AlertComponent +import AlertCheckComponent +import AvatarComponent +import MultilineTextComponent +import BundleIconComponent +import PlainButtonComponent public func webAppLaunchConfirmationController( context: AccountContext, @@ -378,50 +26,209 @@ public func webAppLaunchConfirmationController( completion: @escaping (Bool) -> Void, showMore: (() -> Void)?, openTerms: @escaping () -> Void -) -> AlertController { - let theme = defaultDarkColorPresentationTheme - let presentationData: PresentationData - if let updatedPresentationData { - presentationData = updatedPresentationData.initial - } else { - presentationData = context.sharedContext.currentPresentationData.with { $0 } - } +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings + + let checkState = AlertCheckComponent.ExternalState() - var dismissImpl: ((Bool) -> Void)? - var getContentNodeImpl: (() -> WebAppLaunchConfirmationAlertContentNode?)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_LaunchOpenApp, action: { - if requestWriteAccess, let allowWriteAccess = getContentNodeImpl?()?.allowWriteAccess { - completion(allowWriteAccess) - } else { - completion(false) - } - dismissImpl?(true) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - })] - - let title = peer.compactDisplayTitle - let text = presentationData.strings.WebApp_LaunchTermsConfirmation - - let contentNode = WebAppLaunchConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peer: peer, title: title, text: text, showMore: showMore != nil, requestWriteAccess: requestWriteAccess, actions: actions, morePressed: { - dismissImpl?(true) - showMore?() - }, termsPressed: { - dismissImpl?(true) - openTerms() - }) - getContentNodeImpl = { [weak contentNode] in - return contentNode + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertWebAppHeaderComponent(context: context, peer: peer, showMore: showMore) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.WebApp_LaunchTermsConfirmation)) + ) + )) + if requestWriteAccess { + content.append(AnyComponentWithIdentity( + id: "check", + component: AnyComponent( + AlertCheckComponent(title: strings.WebApp_AddToAttachmentAllowMessages(peer.compactDisplayTitle).string, initialValue: false, externalState: checkState) + ) + )) } - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + let alertController = AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: strings.WebApp_LaunchOpenApp, type: .default, action: { + completion(requestWriteAccess && checkState.value) + }), + .init(title: strings.Common_Cancel) + ] + ) + return alertController +} + +private final class AlertWebAppHeaderComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + let context: AccountContext + let peer: EnginePeer + let showMore: (() -> Void)? + + public init( + context: AccountContext, + peer: EnginePeer, + showMore: (() -> Void)? + ) { + self.context = context + self.peer = peer + self.showMore = showMore + } + + public static func ==(lhs: AlertWebAppHeaderComponent, rhs: AlertWebAppHeaderComponent) -> Bool { + return true + } + + public final class View: UIView { + private let avatar = ComponentView() + private let title = ComponentView() + private let titleIcon = ComponentView() + private let showMore = ComponentView() + + private var component: AlertWebAppHeaderComponent? + private weak var state: EmptyComponentState? + + func update(component: AlertWebAppHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + var contentHeight: CGFloat = 0.0 + let avatarSize = self.avatar.update( + transition: .immediate, + component: AnyComponent( + AvatarComponent( + context: component.context, + theme: environment.theme, + peer: component.peer + ) + ), + environment: {}, + containerSize: CGSize(width: 60.0, height: 60.0) + ) + let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: contentHeight), size: avatarSize) + if let avatarView = self.avatar.view { + if avatarView.superview == nil { + self.addSubview(avatarView) + } + avatarView.frame = avatarFrame + } + contentHeight += avatarSize.height + contentHeight += 17.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.peer.compactDisplayTitle, + font: Font.bold(17.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + horizontalAlignment: .natural, + maximumNumberOfLines: 0 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 32.0, height: availableSize.height) + ) + + var totalWidth = titleSize.width + + var statusContent: EmojiStatusComponent.Content? + if component.peer.isScam { + statusContent = .text(color: environment.theme.list.itemDestructiveColor, string: environment.strings.Message_ScamAccount.uppercased()) + } else if component.peer.isFake { + statusContent = .text(color: environment.theme.list.itemDestructiveColor, string: environment.strings.Message_FakeAccount.uppercased()) + } else if component.peer.isVerified { + statusContent = .verified(fillColor: environment.theme.list.itemCheckColors.fillColor, foregroundColor: environment.theme.list.itemCheckColors.foregroundColor, sizeType: .large) + } + if let statusContent { + let titleIconSize = self.titleIcon.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: statusContent, + isVisibleForAnimations: true, + action: { + } + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + totalWidth += titleIconSize.width + 2.0 + if let titleIconView = self.titleIcon.view { + if titleIconView.superview == nil { + self.addSubview(titleIconView) + } + transition.setFrame(view: titleIconView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0) + titleSize.width + 2.0, y: contentHeight), size: titleIconSize)) + } + } + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0), y: contentHeight), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + contentHeight += titleSize.height + + if let showMore = component.showMore { + contentHeight += 6.0 + + let showMoreSize = self.showMore.update( + transition: .immediate, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "label", component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.WebApp_LaunchMoreInfo, font: Font.regular(14.0), textColor: environment.theme.actionSheet.controlAccentColor))))), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent(BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: environment.theme.actionSheet.controlAccentColor))) + ], spacing: 3.0) + ), + action: { + showMore() + }, + animateScale: false + ) + ), + environment: {}, + containerSize: availableSize + ) + let showMoreFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - showMoreSize.width) / 2.0), y: contentHeight), size: showMoreSize) + if let showMoreView = self.showMore.view { + if showMoreView.superview == nil { + self.addSubview(showMoreView) + } + showMoreView.frame = showMoreFrame + } + contentHeight += showMoreSize.height + } + contentHeight += 12.0 + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + 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/WebUI/Sources/WebAppLocationAlertController.swift b/submodules/WebUI/Sources/WebAppLocationAlertController.swift index 867841fc99..1fe62d6073 100644 --- a/submodules/WebUI/Sources/WebAppLocationAlertController.swift +++ b/submodules/WebUI/Sources/WebAppLocationAlertController.swift @@ -10,17 +10,18 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle -import AvatarNode -import Markdown -import CheckNode +import ComponentFlow +import AlertComponent +import AvatarComponent +import AlertTransferHeaderComponent -private func generateBoostIcon(theme: PresentationTheme) -> UIImage? { - let size = CGSize(width: 28.0, height: 28.0) +private func generateLocationIcon() -> UIImage? { + let size = CGSize(width: 24.0, height: 24.0) return generateImage(size, contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) - context.addEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) + context.addEllipse(in: bounds) context.clip() var locations: [CGFloat] = [1.0, 0.0] @@ -32,261 +33,61 @@ private func generateBoostIcon(theme: PresentationTheme) -> UIImage? { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Location"), color: .white), let cgImage = image.cgImage { - context.draw(cgImage, in: bounds.insetBy(dx: 6.0, dy: 6.0)) + context.draw(cgImage, in: bounds.insetBy(dx: 4.0, dy: 4.0)) } context.resetClip() - - let lineWidth = 2.0 - UIScreenPixel - context.setLineWidth(lineWidth) - context.setStrokeColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) - context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel)) }, opaque: false) } -private final class WebAppLocationAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let text: String - - private let textNode: ASTextNode - private let avatarNode: AvatarNode - private let arrowNode: ASImageNode - private let secondAvatarNode: AvatarNode - private let iconNode: ASImageNode - - 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, accountPeer: EnginePeer, botPeer: EnginePeer, text: String, actions: [TextAlertAction]) { - self.strings = strings - self.text = text - - 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.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.image = generateBoostIcon(theme: ptheme) - - 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.textNode) - self.addSubnode(self.avatarNode) - self.addSubnode(self.arrowNode) - self.addSubnode(self.secondAvatarNode) - self.addSubnode(self.iconNode) - - 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: accountPeer) - self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: botPeer) - } - - override func updateTheme(_ theme: AlertControllerTheme) { - 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) - - if let icon = self.iconNode.image { - let iconFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 4.0 - icon.size.width, y: avatarFrame.maxY + 4.0 - icon.size.height), size: icon.size) - transition.updateFrame(node: self.iconNode, frame: iconFrame) - } - - origin.y += avatarSize.height + 10.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 + 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 webAppLocationAlertController(context: AccountContext, accountPeer: EnginePeer, botPeer: EnginePeer, completion: @escaping (Bool) -> Void) -> AlertController { +func webAppLocationAlertController(context: AccountContext, accountPeer: EnginePeer, botPeer: EnginePeer, completion: @escaping (Bool) -> Void) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - - let text = strings.WebApp_LocationPermission_Text(botPeer.compactDisplayTitle, botPeer.compactDisplayTitle).string - - var dismissImpl: ((Bool) -> Void)? - var contentNode: WebAppLocationAlertContentNode? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: strings.WebApp_LocationPermission_Decline, action: { - dismissImpl?(true) - completion(false) - }), TextAlertAction(type: .defaultAction, title: strings.WebApp_LocationPermission_Allow, action: { - dismissImpl?(true) - completion(true) - })] - contentNode = WebAppLocationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, accountPeer: accountPeer, botPeer: botPeer, text: text, actions: actions) + let locationIcon = generateLocationIcon() - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertTransferHeaderComponent( + fromComponent: AnyComponentWithIdentity(id: "user", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: accountPeer, + icon: AnyComponent(Image(image: locationIcon, contentMode: .center)) + ) + )), + toComponent: AnyComponentWithIdentity(id: "bot", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: botPeer + ) + )), + type: .transfer + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.WebApp_LocationPermission_Text(botPeer.compactDisplayTitle, botPeer.compactDisplayTitle).string)) + ) + )) + + let alertController = AlertScreen( + context: context, + content: content, + actions: [ + .init(title: strings.WebApp_LocationPermission_Decline, action: { + completion(false) + }), + .init(title: strings.WebApp_LocationPermission_Allow, type: .default, action: { + completion(true) + }) + ] + ) + return alertController } diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift index 9d76690c30..3d65b1f3f2 100644 --- a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -256,13 +256,14 @@ private final class SheetContent: CombinedComponent { let buttonString: String = environment.strings.WebApp_ShareMessage_Share let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + 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), @@ -276,7 +277,7 @@ private final class SheetContent: CombinedComponent { } } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: .immediate ) context.add(button @@ -285,10 +286,8 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) ) contentSize.height += button.size.height - contentSize.height += 15.0 + contentSize.height += buttonInsets.bottom - contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) - return contentSize } } @@ -338,6 +337,7 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent { let controller = environment.controller + let theme = environment.theme.withModalBlocksBackground() let sheet = sheet.update( component: SheetComponent( content: AnyComponent(SheetContent( @@ -355,7 +355,7 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent { } )), style: .glass, - backgroundColor: .color(environment.theme.list.blocksBackgroundColor), + backgroundColor: .color(theme.list.blocksBackgroundColor), followContentSizeChanges: false, clipsContent: true, isScrollEnabled: false, diff --git a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift index 901c907cc1..55cd331d46 100644 --- a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift +++ b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift @@ -198,6 +198,7 @@ private final class SheetContent: CombinedComponent { let controller = environment.controller() as? WebAppSetEmojiStatusScreen + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( @@ -217,7 +218,7 @@ private final class SheetContent: CombinedComponent { controller?.dismissAnimated() } ), - availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: .immediate ) context.add(button @@ -226,8 +227,7 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) ) contentSize.height += button.size.height - - contentSize.height += 48.0 + contentSize.height += buttonInsets.bottom return contentSize } diff --git a/submodules/WebUI/Sources/WebAppTermsAlertController.swift b/submodules/WebUI/Sources/WebAppTermsAlertController.swift index 88f3a71776..ba305d3bb7 100644 --- a/submodules/WebUI/Sources/WebAppTermsAlertController.swift +++ b/submodules/WebUI/Sources/WebAppTermsAlertController.swift @@ -9,345 +9,9 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle -import AvatarNode -import CheckNode -import Markdown -import TextFormat - -private let textFont = Font.regular(13.0) -private let boldTextFont = Font.semibold(13.0) - -private func formattedText(_ text: String, fontSize: CGFloat, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { - return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: color), bold: MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: color), link: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: linkColor), linkAttribute: { _ in return (TelegramTextAttributes.URL, "") }), textAlignment: textAlignment) -} - -private final class WebAppTermsAlertContentNode: AlertContentNode, ASGestureRecognizerDelegate { - private let strings: PresentationStrings - private let title: String - private let text: String - private let additionalText: String? - - private let titleNode: ImmediateTextNode - private let textNode: ImmediateTextNode - private let additionalTextNode: ImmediateTextNode - - private let acceptTermsCheckNode: InteractiveCheckNode - private let acceptTermsLabelNode: ImmediateTextNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private var validLayout: CGSize? - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - var acceptedTerms: Bool = false { - didSet { - self.acceptTermsCheckNode.setSelected(self.acceptedTerms, animated: true) - if let firstAction = self.actionNodes.first { - firstAction.actionEnabled = self.acceptedTerms - } - } - } - - var openTerms: () -> Void = {} - - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, additionalText: String?, actions: [TextAlertAction]) { - self.strings = strings - self.title = title - self.text = text - self.additionalText = additionalText - - self.titleNode = ImmediateTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.textAlignment = .center - - self.textNode = ImmediateTextNode() - self.textNode.maximumNumberOfLines = 0 - self.textNode.displaysAsynchronously = false - self.textNode.lineSpacing = 0.1 - self.textNode.textAlignment = .center - - self.additionalTextNode = ImmediateTextNode() - self.additionalTextNode.maximumNumberOfLines = 0 - self.additionalTextNode.displaysAsynchronously = false - self.additionalTextNode.lineSpacing = 0.1 - self.additionalTextNode.textAlignment = .center - - self.acceptTermsCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) - self.acceptTermsLabelNode = ImmediateTextNode() - self.acceptTermsLabelNode.maximumNumberOfLines = 4 - - 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.additionalTextNode) - - self.addSubnode(self.acceptTermsCheckNode) - self.addSubnode(self.acceptTermsLabelNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.acceptTermsCheckNode.valueChanged = { [weak self] value in - if let strongSelf = self { - strongSelf.acceptedTerms = !strongSelf.acceptedTerms - } - } - - self.acceptTermsLabelNode.highlightAttributeAction = { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - } - self.acceptTermsLabelNode.tapAttributeAction = { [weak self] attributes, _ in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - self?.openTerms() - } - } - - self.updateTheme(theme) - } - - override func didLoad() { - super.didLoad() - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:))) - tapGesture.delegate = self.wrappedGestureRecognizerDelegate - self.view.addGestureRecognizer(tapGesture) - - if let firstAction = self.actionNodes.first { - firstAction.actionEnabled = false - } - } - - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let location = gestureRecognizer.location(in: self.acceptTermsLabelNode.view) - if self.acceptTermsLabelNode.bounds.contains(location) { - return true - } - return false - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - - if let (_, attributes) = self.acceptTermsLabelNode.attributesAtPoint(self.view.convert(point, to: self.acceptTermsLabelNode.view)) { - if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil { - return self.view - } - } - - return super.hitTest(point, with: event) - } - - @objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) { - let location = gestureRecognizer.location(in: self.acceptTermsLabelNode.view) - if self.acceptTermsCheckNode.isUserInteractionEnabled { - if let attributes = self.acceptTermsLabelNode.attributesAtPoint(location)?.1 { - if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil { - return - } - } - self.acceptedTerms = !self.acceptedTerms - } - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = formattedText(self.text, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center) - if let additionalText = self.additionalText { - self.additionalTextNode.attributedText = formattedText(additionalText, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center) - } else { - self.additionalTextNode.attributedText = nil - } - - let attributedAgreeText = parseMarkdownIntoAttributedString( - self.strings.WebApp_DisclaimerAgree, - attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor), - bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.primaryColor), - link: MarkdownAttributeSet(font: textFont, textColor: theme.accentColor), - linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - } - ) - ) - - self.acceptTermsLabelNode.attributedText = attributedAgreeText - self.acceptTermsLabelNode.linkHighlightColor = theme.accentColor.withAlphaComponent(0.2) - - 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: 17.0) - - let titleSize = self.titleNode.updateLayout(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 - - var entriesHeight: CGFloat = 0.0 - - let textSize = self.textNode.updateLayout(CGSize(width: size.width - 48.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 - - if self.acceptTermsLabelNode.supernode != nil { - origin.y += 21.0 - entriesHeight += 21.0 - - let checkSize = CGSize(width: 22.0, height: 22.0) - let condensedSize = CGSize(width: size.width - 76.0, height: size.height) - - let spacing: CGFloat = 12.0 - let acceptTermsSize = self.acceptTermsLabelNode.updateLayout(condensedSize) - let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width - let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0) - - transition.updateFrame(node: self.acceptTermsCheckNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize)) - transition.updateFrame(node: self.acceptTermsLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize)) - origin.y += acceptTermsSize.height - entriesHeight += acceptTermsSize.height - origin.y += 21.0 - } - - let additionalTextSize = self.additionalTextNode.updateLayout(CGSize(width: size.width - 48.0, height: size.height)) - transition.updateFrame(node: self.additionalTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - additionalTextSize.width) / 2.0), y: origin.y), size: additionalTextSize)) - origin.y += additionalTextSize.height - if additionalTextSize.height > 0.0 { - entriesHeight += 20.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.vertical - 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: titleSize.height + textSize.height + additionalTextSize.height + entriesHeight + actionsHeight + 3.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 - } -} +import ComponentFlow +import AlertComponent +import AlertCheckComponent public func webAppTermsAlertController( context: AccountContext, @@ -355,46 +19,51 @@ public func webAppTermsAlertController( bot: AttachMenuBot, completion: @escaping (Bool) -> Void, dismissed: @escaping () -> Void = {} -) -> AlertController { - let theme = defaultDarkColorPresentationTheme - let presentationData: PresentationData - if let updatedPresentationData { - presentationData = updatedPresentationData.initial - } else { - presentationData = context.sharedContext.currentPresentationData.with { $0 } - } +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings + + let checkState = AlertCheckComponent.ExternalState() - var dismissImpl: ((Bool) -> Void)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_DisclaimerContinue, action: { - completion(true) - dismissImpl?(true) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissed() - dismissImpl?(true) - })] + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.WebApp_DisclaimerTitle) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.WebApp_DisclaimerText)) + ) + )) + content.append(AnyComponentWithIdentity( + id: "check", + component: AnyComponent( + AlertCheckComponent(title: strings.WebApp_DisclaimerAgree, initialValue: false, externalState: checkState, linkAction: { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: strings.WebApp_Disclaimer_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + }) + ) + )) - let title = presentationData.strings.WebApp_DisclaimerTitle - let text = presentationData.strings.WebApp_DisclaimerText - let additionalText: String? = nil + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) + } - let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, additionalText: additionalText, actions: actions) - contentNode.openTerms = { - context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_Disclaimer_URL, forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: { - }) - } - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - controller.dismissed = { outside in - if outside { - dismissed() - } - } - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: strings.WebApp_DisclaimerContinue, type: .default, action: { + completion(checkState.value) + }, isEnabled: checkState.valueSignal), + .init(title: strings.Common_Cancel) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + return alertController } From 2bba31bcdf690b916b343d39a3b4c6c8b0a92e2e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 22 Dec 2025 23:28:04 +0400 Subject: [PATCH 2/6] Alerts --- submodules/AuthorizationUI/BUILD | 1 + ...orizationSequenceCodeEntryController.swift | 13 +++- .../AuthorizationSequenceController.swift | 58 +++++++++--------- ...ationSequencePasswordEntryController.swift | 12 ++-- .../AuthorizationSequencePaymentScreen.swift | 2 +- ...rizationSequencePhoneEntryController.swift | 5 +- ...uthorizationSequenceSignUpController.swift | 10 +++- .../Sources/ChatListController.swift | 18 +++--- .../ChatListFilterPresetController.swift | 8 +-- .../ChatListFilterPresetListController.swift | 7 ++- .../Sources/ChatListSearchContainerNode.swift | 2 +- .../Sources/MediaPickerScreen.swift | 18 +++--- .../Sources/NotificationSoundSelection.swift | 2 +- .../TwoFactorAuthDataInputScreen.swift | 10 ++-- .../Sources/ChannelAdminController.swift | 2 +- .../Sources/ChangePhoneNumberController.swift | 4 +- .../ProxyServerActionSheetController.swift | 20 ++++--- .../GlobalAutoremoveScreen.swift | 8 +-- .../LoginEmailSetupController.swift | 11 ++-- .../TermsOfServiceController.swift | 10 +++- .../Sources/PresentationGroupCall.swift | 7 ++- .../Sources/AdminUserActionsSheet.swift | 3 +- .../Chat/ForwardAccessoryPanelNode/BUILD | 2 + .../Sources/ForwardAccessoryPanelNode.swift | 60 +++++++++++++------ .../Sources/ChatFolderLinkPreviewScreen.swift | 2 +- .../NotificationPeerExceptionController.swift | 2 +- .../Sources/PeerAllowedReactionsScreen.swift | 5 +- .../Sources/AffiliateProgramSetupScreen.swift | 4 +- .../Sources/PeerInfoScreen.swift | 4 +- .../Sources/PeerInfoStoryPaneNode.swift | 5 +- .../AutomaticBusinessMessageSetupScreen.swift | 4 +- .../Sources/BusinessLinksSetupScreen.swift | 3 +- .../Sources/BusinessDaySetupScreen.swift | 3 +- .../Sources/BusinessHoursSetupScreen.swift | 3 +- .../Sources/BusinessLocationSetupScreen.swift | 6 +- .../Sources/ChatbotSetupScreen.swift | 2 +- ...StoryItemSetContainerViewSendMessage.swift | 8 +-- .../TelegramUI/Sources/AppDelegate.swift | 2 +- .../Sources/ApplicationContext.swift | 2 +- .../ChatInterfaceStateAccessoryPanels.swift | 54 ++++++++++++----- 40 files changed, 238 insertions(+), 164 deletions(-) diff --git a/submodules/AuthorizationUI/BUILD b/submodules/AuthorizationUI/BUILD index 5a48a9b381..cc280f1551 100644 --- a/submodules/AuthorizationUI/BUILD +++ b/submodules/AuthorizationUI/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/TelegramUI/Components/Premium/PremiumCoinComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/Utils/DeviceModel", + "//submodules/PresentationDataUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift index 6cd8ce982d..72f1db4291 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift @@ -5,7 +5,9 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData +import PresentationDataUtils import ProgressNavigationButtonNode +import AccountContext public final class AuthorizationSequenceCodeEntryController: ViewController { private var controllerNode: AuthorizationSequenceCodeEntryControllerNode { @@ -14,6 +16,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { private var validLayout: ContainerViewLayout? + private let sharedContext: SharedAccountContext private let strings: PresentationStrings private let theme: PresentationTheme @@ -45,7 +48,8 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { return self.data?.6 ?? false } - public init(presentationData: PresentationData, back: @escaping () -> Void) { + public init(sharedContext: SharedAccountContext, presentationData: PresentationData, back: @escaping () -> Void) { + self.sharedContext = sharedContext self.strings = presentationData.strings self.theme = presentationData.theme @@ -61,11 +65,14 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { return false } self.navigationBar?.backPressed = { [weak self] in + guard let self else { + return + } let text: String let proceed: String let stop: String - if let (_, _, type, _, _, _, _) = self?.data, case .email = type { + if let (_, _, type, _, _, _, _) = self.data, case .email = type { text = presentationData.strings.Login_CancelEmailVerification proceed = presentationData.strings.Login_CancelEmailVerificationContinue stop = presentationData.strings.Login_CancelEmailVerificationStop @@ -75,7 +82,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { stop = presentationData.strings.Login_CancelPhoneVerificationStop } - self?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: proceed, action: { + self.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: proceed, action: { }), TextAlertAction(type: .defaultAction, title: stop, action: { back() })]), in: .window(.root)) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index ce6947eb0c..c4c0fc7b20 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -9,6 +9,7 @@ import MtProtoKit import MessageUI import CoreTelephony import TelegramPresentationData +import PresentationDataUtils import TextFormat import AccountContext import CountrySelectionUI @@ -247,7 +248,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let carrier = CTCarrier() let mnc = carrier.mobileNetworkCode ?? "none" - AuthorizationSequenceController.presentEmailComposeController(address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_InvalidPhoneEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_InvalidPhoneEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) + AuthorizationSequenceController.presentEmailComposeController(sharedContext: strongSelf.sharedContext, address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_InvalidPhoneEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_InvalidPhoneEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) })) case .phoneLimitExceeded: text = strongSelf.presentationData.strings.Login_PhoneFloodError @@ -273,7 +274,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let carrier = CTCarrier() let mnc = carrier.mobileNetworkCode ?? "none" - AuthorizationSequenceController.presentEmailComposeController(address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) + AuthorizationSequenceController.presentEmailComposeController(sharedContext: strongSelf.sharedContext, address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) })) case let .generic(info): text = strongSelf.presentationData.strings.Login_UnknownError @@ -295,7 +296,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth errorString = "unknown" } - AuthorizationSequenceController.presentEmailComposeController(address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneGenericEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneGenericEmailBody(formattedNumber, errorString, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) + AuthorizationSequenceController.presentEmailComposeController(sharedContext: strongSelf.sharedContext, address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneGenericEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneGenericEmailBody(formattedNumber, errorString, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData) })) case .timeout: text = strongSelf.presentationData.strings.Login_NetworkError @@ -307,7 +308,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.present(strongSelf.sharedContext.makeProxySettingsController(sharedContext: strongSelf.sharedContext, account: strongSelf.account), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) } - (controller.navigationController as? NavigationController)?.presentOverlay(controller: standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: actions), inGlobal: true, blockInteraction: true) + (controller.navigationController as? NavigationController)?.presentOverlay(controller: textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: actions), inGlobal: true, blockInteraction: true) controller.dismissConfirmation() } @@ -352,7 +353,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth text = strongSelf.presentationData.strings.Login_UnknownError } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -376,7 +377,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceCodeEntryController(presentationData: self.presentationData, back: { [weak self] in + controller = AuthorizationSequenceCodeEntryController(sharedContext: self.sharedContext, presentationData: self.presentationData, back: { [weak self] in guard let strongSelf = self else { return } @@ -451,7 +452,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let _ = self.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() } - controller.presentInGlobalOverlay(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})])) + controller.presentInGlobalOverlay(textAlertController(sharedContext: self.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})])) } }) ) @@ -510,7 +511,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.resetCode() } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } } @@ -623,7 +624,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.resetCode() } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } } @@ -645,7 +646,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let mnc = carrier.mobileNetworkCode ?? "none" let _ = strongSelf.engine.auth.reportMissingCode(phoneNumber: number, phoneCodeHash: phoneCodeHash, mnc: mnc).start() - AuthorizationSequenceController.presentDidNotGetCodeUI(controller: controller, presentationData: strongSelf.presentationData, phoneNumber: number, mnc: mnc) + AuthorizationSequenceController.presentDidNotGetCodeUI(sharedContext: strongSelf.sharedContext, controller: controller, presentationData: strongSelf.presentationData, phoneNumber: number, mnc: mnc) } } else { controller?.inProgress = true @@ -681,7 +682,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth text = strongSelf.presentationData.strings.Login_NetworkError } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: actions), in: .window(.root)) + controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: actions), in: .window(.root)) } })) } @@ -780,7 +781,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth text = strongSelf.presentationData.strings.Login_EmailNotAllowedError } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } }, completed: { controller?.inProgress = false @@ -827,7 +828,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth switch authorization.credential { case let appleIdCredential as ASAuthorizationAppleIDCredential: guard let tokenData = appleIdCredential.identityToken, let token = String(data: tokenData, encoding: .utf8) else { - lastController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + lastController?.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return } @@ -850,7 +851,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth case .emailNotAllowed: text = strongSelf.presentationData.strings.Login_EmailNotAllowedError } - lastController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + lastController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } else { @@ -891,7 +892,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth text = strongSelf.presentationData.strings.Login_InvalidEmailAddressError } - lastController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + lastController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } }) @@ -907,7 +908,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth guard let lastController = self.viewControllers.last as? ViewController else { return } - lastController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: error.localizedDescription, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + lastController.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: error.localizedDescription, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } @available(iOS 13.0, *) @@ -927,7 +928,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequencePasswordEntryController(presentationData: self.presentationData, back: { [weak self] in + controller = AuthorizationSequencePasswordEntryController(sharedContext: self.sharedContext, presentationData: self.presentationData, back: { [weak self] in guard let strongSelf = self else { return } @@ -954,7 +955,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth text = strongSelf.presentationData.strings.Login_UnknownError } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) controller.passwordIsInvalid() } } @@ -988,14 +989,14 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth strongController.inProgress = false - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) strongController.didForgotWithNoRecovery = true })) } } controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: suggestReset ? strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed : strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [ + strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: suggestReset ? strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed : strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { @@ -1015,7 +1016,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth case .limitExceeded: text = strongSelf.presentationData.strings.Login_ResetAccountProtected_LimitExceeded } - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -1082,7 +1083,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth }) controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ + strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { @@ -1102,7 +1103,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth case .limitExceeded: text = strongSelf.presentationData.strings.Login_ResetAccountProtected_LimitExceeded } - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -1132,7 +1133,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceSignUpController(presentationData: self.presentationData, back: { [weak self] in + controller = AuthorizationSequenceSignUpController(sharedContext: self.sharedContext, presentationData: self.presentationData, back: { [weak self] in guard let strongSelf = self else { return } @@ -1231,7 +1232,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth text = strongSelf.presentationData.strings.Login_UnknownError } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -1379,7 +1380,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } } - static func presentEmailComposeController(address: String, subject: String, body: String, from controller: ViewController, presentationData: PresentationData) { + static func presentEmailComposeController(sharedContext: SharedAccountContext, address: String, subject: String, body: String, from controller: ViewController, presentationData: PresentationData) { if MFMailComposeViewController.canSendMail() { final class ComposeDelegate: NSObject, MFMailComposeViewControllerDelegate { @objc func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { @@ -1398,7 +1399,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.view.window?.rootViewController?.present(composeController, animated: true, completion: nil) } else { - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(textAlertController(sharedContext: sharedContext, title: nil, text: presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } @@ -1451,6 +1452,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } public static func presentDidNotGetCodeUI( + sharedContext: SharedAccountContext, controller: ViewController, presentationData: PresentationData, phoneNumber: String, @@ -1470,6 +1472,6 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth emailBody.append("Locale: \(locale)\n") emailBody.append("MNC: \(mnc)") - AuthorizationSequenceController.presentEmailComposeController(address: "sms@telegram.org", subject: presentationData.strings.Login_EmailCodeSubject(formattedNumber).string, body: emailBody, from: controller, presentationData: presentationData) + AuthorizationSequenceController.presentEmailComposeController(sharedContext: sharedContext, address: "sms@telegram.org", subject: presentationData.strings.Login_EmailCodeSubject(formattedNumber).string, body: emailBody, from: controller, presentationData: presentationData) } } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift index 000a0a49db..913678b1a1 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift @@ -3,7 +3,9 @@ import UIKit import Display import AsyncDisplayKit import TelegramPresentationData +import PresentationDataUtils import ProgressNavigationButtonNode +import AccountContext final class AuthorizationSequencePasswordEntryController: ViewController { private var controllerNode: AuthorizationSequencePasswordEntryControllerNode { @@ -12,6 +14,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { private var validLayout: ContainerViewLayout? + private let sharedContext: SharedAccountContext private let presentationData: PresentationData var loginWithPassword: ((String) -> Void)? @@ -40,7 +43,8 @@ final class AuthorizationSequencePasswordEntryController: ViewController { } } - init(presentationData: PresentationData, back: @escaping () -> Void) { + init(sharedContext: SharedAccountContext, presentationData: PresentationData, back: @escaping () -> Void) { + self.sharedContext = sharedContext self.presentationData = presentationData super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) @@ -153,10 +157,8 @@ final class AuthorizationSequencePasswordEntryController: ViewController { } func forgotPressed() { - /*if self.suggestReset { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } else*/ if self.didForgotWithNoRecovery { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + if self.didForgotWithNoRecovery { + self.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } else { self.forgot?() } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift index f261be57a9..61715e95a0 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -215,7 +215,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { ).string let presentationData = component.presentationData - AuthorizationSequenceController.presentEmailComposeController(address: component.supportEmailAddress, subject: environment.strings.Login_PhonePaidEmailSubject, body: body, from: controller, presentationData: presentationData) + AuthorizationSequenceController.presentEmailComposeController(sharedContext: component.sharedContext, address: component.supportEmailAddress, subject: environment.strings.Login_PhonePaidEmailSubject, body: body, from: controller, presentationData: presentationData) } func update(component: AuthorizationSequencePaymentScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift index 54855476a3..8d255bf1a1 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -6,6 +6,7 @@ import SwiftSignalKit import TelegramCore import Postbox import TelegramPresentationData +import PresentationDataUtils import ProgressNavigationButtonNode import AccountContext import CountrySelectionUI @@ -404,7 +405,7 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF })) } actions.append(TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})) - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorized, actions: actions), in: .window(.root)) + self.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorized, actions: actions), in: .window(.root)) } else { if let validLayout = self.validLayout, validLayout.size.width > 320.0 { let (code, formattedNumber) = self.controllerNode.formattedCodeAndNumber @@ -425,7 +426,7 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF strongSelf.loginWithNumber?(strongSelf.controllerNode.currentNumber, strongSelf.controllerNode.syncContacts) } })) - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: logInNumber, text: self.presentationData.strings.Login_PhoneNumberConfirmation, actions: actions), in: .window(.root)) + self.present(textAlertController(sharedContext: self.sharedContext, title: logInNumber, text: self.presentationData.strings.Login_PhoneNumberConfirmation, actions: actions), in: .window(.root)) } } } else { diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift index 8c8fdd8df9..2ec0d70541 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData +import PresentationDataUtils import LegacyComponents import ProgressNavigationButtonNode import ImageCompression @@ -13,6 +14,7 @@ import Postbox import TextFormat import MoreButtonNode import ContextUI +import AccountContext final class AuthorizationSequenceSignUpController: ViewController { private var controllerNode: AuthorizationSequenceSignUpControllerNode { @@ -23,6 +25,7 @@ final class AuthorizationSequenceSignUpController: ViewController { private let moreButtonNode: MoreButtonNode + private let sharedContext: SharedAccountContext private let presentationData: PresentationData private let back: () -> Void @@ -46,7 +49,8 @@ final class AuthorizationSequenceSignUpController: ViewController { } } - init(presentationData: PresentationData, back: @escaping () -> Void, displayCancel: Bool) { + init(sharedContext: SharedAccountContext, presentationData: PresentationData, back: @escaping () -> Void, displayCancel: Bool) { + self.sharedContext = sharedContext self.presentationData = presentationData self.back = back @@ -68,7 +72,7 @@ final class AuthorizationSequenceSignUpController: ViewController { guard let strongSelf = self else { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Login_CancelPhoneVerificationContinue, action: { + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Login_CancelPhoneVerificationContinue, action: { }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_CancelPhoneVerificationStop, action: { back() })]), in: .window(.root)) @@ -92,7 +96,7 @@ final class AuthorizationSequenceSignUpController: ViewController { } @objc private func cancelPressed() { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_CancelPhoneVerificationContinue, action: { + self.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: self.presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_CancelPhoneVerificationContinue, action: { }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Login_CancelPhoneVerificationStop, action: { [weak self] in self?.back() })]), in: .window(.root)) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 60884e2102..287700a955 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2635,7 +2635,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } strongSelf.didSuggestAutoarchive = true - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_AutoarchiveSuggestion_Title, text: strongSelf.presentationData.strings.ChatList_AutoarchiveSuggestion_Text, actions: [ + strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_AutoarchiveSuggestion_Title, text: strongSelf.presentationData.strings.ChatList_AutoarchiveSuggestion_Text, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { guard let strongSelf = self else { return @@ -2687,7 +2687,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return true } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissTitle, text: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissText(value), actions: [ + controller.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissTitle, text: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissText(value), actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissActionCancel, action: { }), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissActionOK, action: { [weak controller] in @@ -3101,7 +3101,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } func displayContinueLiveStream() { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.ChatList_AlertResumeLiveStreamTitle, text: self.presentationData.strings.ChatList_AlertResumeLiveStreamText, actions: [ + self.present(textAlertController(context: self.context, title: self.presentationData.strings.ChatList_AlertResumeLiveStreamTitle, text: self.presentationData.strings.ChatList_AlertResumeLiveStreamText, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatList_AlertResumeLiveStreamAction, action: { [weak self] in @@ -4638,7 +4638,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if hasLinks { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [ + self.present(textAlertController(context: self.context, title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [ TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { confirmDeleteFolder() }), @@ -5500,7 +5500,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController deleteForAllConfirmation = strongSelf.presentationData.strings.ChannelInfo_DeleteGroupConfirmation } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [ + strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { @@ -5615,7 +5615,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ]) strongSelf.present(actionSheet, in: .window(.root)) } else { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { @@ -5683,7 +5683,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController deleteForAllConfirmation = strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [ + strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { @@ -5891,7 +5891,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [ + strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { completion(false) }), @@ -5915,7 +5915,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ]) self.present(actionSheet, in: .window(.root)) } else if peer.peerId == self.context.account.peerId { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + self.present(textAlertController(context: self.context, title: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { completion(false) }), diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 65cd480d28..da255877f8 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1752,14 +1752,14 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi if initialPreset == nil { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let text = presentationData.strings.ChatListFilter_AlertCreateFolderBeforeSharingText - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let state = stateValue.with({ $0 }) if state.additionallyIncludePeers.isEmpty { let text = presentationData.strings.ChatListFilter_ErrorShareInvalidFolder - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) return } @@ -1784,7 +1784,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi statusController?.dismiss() let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + presentControllerImpl?(textAlertController(context: context, title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) return } @@ -2359,7 +2359,7 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec case .someUserTooManyChannels: text = presentationData.strings.ChatListFilter_CreateLinkErrorSomeoneHasChannelLimit } - presentController(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + presentController(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) }) } }) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 56c5c9ba62..8750c6f8a5 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -5,6 +5,7 @@ import SwiftSignalKit import TelegramCore import TelegramPresentationData import TelegramUIPreferences +import PresentationDataUtils import ItemListUI import AccountContext import ItemListPeerActionItem @@ -516,13 +517,13 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } if hasLinks { - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [ + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [ TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { confirmDeleteFolder() }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) - ])) + ], actionLayout: .vertical)) } else { confirmDeleteFolder() } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 7cd8cfb0b3..9ff2c6e247 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -1352,7 +1352,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo title = strongSelf.presentationData.strings.DownloadList_RemoveFileAlertTitle(Int32(messages.count)) text = strongSelf.presentationData.strings.DownloadList_RemoveFileAlertText(Int32(messages.count)) - strongSelf.present?(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [ + strongSelf.present?(textAlertController(context: strongSelf.context, title: title, text: text, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.DownloadList_RemoveFileAlertRemove, action: { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 7f1b2dbc9c..8c3c16355a 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1437,7 +1437,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } if asFile && hasHeic { - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.MediaPicker_KeepHeic, action: { + controller.present(textAlertController(context: controller.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.MediaPicker_KeepHeic, action: { proceed(false) }), TextAlertAction(type: .genericAction, title: self.presentationData.strings.MediaPicker_ConvertToJpeg, action: { proceed(true) @@ -2043,26 +2043,26 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att if let _ = item as? TGMediaPickerGalleryPhotoItem { if self.bannedSendPhotos != nil { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else if let _ = item as? TGMediaPickerGalleryVideoItem { if self.bannedSendVideos != nil { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else if let asset = item as? TGMediaAsset { if asset.isVideo { if self.bannedSendVideos != nil { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else { if self.bannedSendPhotos != nil { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } @@ -2221,26 +2221,26 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att if let self = self, let selectionState = self.interaction?.selectionState { if let _ = item as? TGMediaPickerGalleryPhotoItem { if self.bannedSendPhotos != nil { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else if let _ = item as? TGMediaPickerGalleryVideoItem { if self.bannedSendVideos != nil { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else if let asset = item as? TGMediaAsset { if asset.isVideo { if self.bannedSendVideos != nil { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else { if self.bannedSendPhotos != nil { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } diff --git a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift index 4e9432cc77..a744c229dd 100644 --- a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift +++ b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift @@ -455,7 +455,7 @@ public func notificationSoundSelectionController(context: AccountContext, update let presentationData = context.sharedContext.currentPresentationData.with { $0 } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.PeerInfo_DeleteToneTitle, text: presentationData.strings.PeerInfo_DeleteToneText(title).string, actions: [ + controller.present(textAlertController(context: context, title: presentationData.strings.PeerInfo_DeleteToneTitle, text: presentationData.strings.PeerInfo_DeleteToneText(title).string, actions: [ TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { updateState { state in var state = state diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index aa2e38e670..54843c67d3 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -119,7 +119,7 @@ public final class TwoFactorDataInputScreen: ViewController { return } if values[0] != values[1] { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [ + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) ]), in: .window(.root)) return @@ -167,7 +167,7 @@ public final class TwoFactorDataInputScreen: ViewController { text = strongSelf.presentationData.strings.Login_UnknownError } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, completed: { [weak statusController] in statusController?.dismiss() @@ -196,7 +196,7 @@ public final class TwoFactorDataInputScreen: ViewController { return } if values[0] != values[1] { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [ + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) ]), in: .window(.root)) return @@ -481,7 +481,7 @@ public final class TwoFactorDataInputScreen: ViewController { } switch strongSelf.mode { case let .emailAddress(password, hint, doneText): - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationText, actions: [ + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationText, actions: [ TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationSkip, action: { guard let strongSelf = self else { return @@ -543,7 +543,7 @@ public final class TwoFactorDataInputScreen: ViewController { strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: "", doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) } case let .passwordRecovery(recovery, _): - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertText, actions: [ + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertText, actions: [ TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertAction, action: { guard let strongSelf = self else { return diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 142f48c511..96e2860b9d 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -1091,7 +1091,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD text = presentationData.strings.Channel_EditAdmin_CannotEdit } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) }, transferOwnership: { let _ = (context.engine.data.get( diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift index d82f5054c0..9a03e4797d 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift @@ -48,7 +48,7 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll controller?.inProgress = false var dismissImpl: (() -> Void)? - let codeController = AuthorizationSequenceCodeEntryController(presentationData: presentationData, back: { + let codeController = AuthorizationSequenceCodeEntryController(sharedContext: context.sharedContext, presentationData: presentationData, back: { dismissImpl?() }) codeController.loginWithCode = { [weak codeController] code in @@ -109,7 +109,7 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll let mnc = carrier.mobileNetworkCode ?? "none" let _ = context.engine.auth.reportMissingCode(phoneNumber: phoneNumber, phoneCodeHash: next.hash, mnc: mnc).start() - AuthorizationSequenceController.presentDidNotGetCodeUI(controller: codeController, presentationData: context.sharedContext.currentPresentationData.with({ $0 }), phoneNumber: phoneNumber, mnc: mnc) + AuthorizationSequenceController.presentDidNotGetCodeUI(sharedContext: context.sharedContext, controller: codeController, presentationData: context.sharedContext.currentPresentationData.with({ $0 }), phoneNumber: phoneNumber, mnc: mnc) } codeController.openFragment = { url in context.sharedContext.applicationBindings.openUrl(url) diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift index f4c1aee48b..bcc32f5847 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift @@ -14,6 +14,7 @@ import PresentationDataUtils import UrlEscaping public final class ProxyServerActionSheetController: ActionSheetController { + private let sharedContext: SharedAccountContext private var presentationDisposable: Disposable? private let _ready = Promise() @@ -25,10 +26,11 @@ public final class ProxyServerActionSheetController: ActionSheetController { convenience public init(context: AccountContext, server: ProxyServerSettings) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.init(presentationData: presentationData, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, updatedPresentationData: context.sharedContext.presentationData) + self.init(sharedContext: context.sharedContext, presentationData: presentationData, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, updatedPresentationData: context.sharedContext.presentationData) } - public init(presentationData: PresentationData, accountManager: AccountManager, postbox: Postbox, network: Network, server: ProxyServerSettings, updatedPresentationData: Signal?) { + public init(sharedContext: SharedAccountContext, presentationData: PresentationData, accountManager: AccountManager, postbox: Postbox, network: Network, server: ProxyServerSettings, updatedPresentationData: Signal?) { + self.sharedContext = sharedContext let sheetTheme = ActionSheetControllerTheme(presentationData: presentationData) super.init(theme: sheetTheme) @@ -39,7 +41,7 @@ public final class ProxyServerActionSheetController: ActionSheetController { items.append(ActionSheetTextItem(title: presentationData.strings.SocksProxySetup_AdNoticeHelp)) } items.append(ProxyServerInfoItem(strings: presentationData.strings, network: network, server: server)) - items.append(ProxyServerActionItem(accountManager:accountManager, postbox: postbox, network: network, presentationData: presentationData, server: server, dismiss: { [weak self] success in + items.append(ProxyServerActionItem(sharedContext: sharedContext, accountManager:accountManager, postbox: postbox, network: network, presentationData: presentationData, server: server, dismiss: { [weak self] success in guard let strongSelf = self, !strongSelf.isDismissed else { return } @@ -262,6 +264,7 @@ private final class ProxyServerInfoItemNode: ActionSheetItemNode { } private final class ProxyServerActionItem: ActionSheetItem { + private let sharedContext: SharedAccountContext private let accountManager: AccountManager private let postbox: Postbox private let network: Network @@ -270,7 +273,8 @@ private final class ProxyServerActionItem: ActionSheetItem { private let dismiss: (Bool) -> Void private let present: (ViewController, Any?) -> Void - init(accountManager: AccountManager, postbox: Postbox, network: Network, presentationData: PresentationData, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(sharedContext: SharedAccountContext, accountManager: AccountManager, postbox: Postbox, network: Network, presentationData: PresentationData, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { + self.sharedContext = sharedContext self.accountManager = accountManager self.postbox = postbox self.network = network @@ -281,7 +285,7 @@ private final class ProxyServerActionItem: ActionSheetItem { } func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ProxyServerActionItemNode(accountManager: self.accountManager, postbox: self.postbox, network: self.network, presentationData: self.presentationData, theme: theme, server: self.server, dismiss: self.dismiss, present: self.present) + return ProxyServerActionItemNode(sharedContext: self.sharedContext, accountManager: self.accountManager, postbox: self.postbox, network: self.network, presentationData: self.presentationData, theme: theme, server: self.server, dismiss: self.dismiss, present: self.present) } func updateNode(_ node: ActionSheetItemNode) { @@ -289,6 +293,7 @@ private final class ProxyServerActionItem: ActionSheetItem { } private final class ProxyServerActionItemNode: ActionSheetItemNode { + private let sharedContext: SharedAccountContext private let accountManager: AccountManager private let postbox: Postbox private let network: Network @@ -305,7 +310,8 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode { private let disposable = MetaDisposable() private var revertSettings: ProxySettings? - init(accountManager: AccountManager, postbox: Postbox, network: Network, presentationData: PresentationData, theme: ActionSheetControllerTheme, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(sharedContext: SharedAccountContext, accountManager: AccountManager, postbox: Postbox, network: Network, presentationData: PresentationData, theme: ActionSheetControllerTheme, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { + self.sharedContext = sharedContext self.accountManager = accountManager self.postbox = postbox self.network = network @@ -430,7 +436,7 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode { strongSelf.buttonNode.isUserInteractionEnabled = true strongSelf.requestLayoutUpdate() - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) } } })) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift index a10a405f7f..6b4d5aae1b 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift @@ -269,8 +269,8 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime) - presentControllerImpl?(standardTextAlertController( - theme: AlertControllerTheme(presentationData: presentationData), + presentControllerImpl?(textAlertController( + context: context, title: presentationData.strings.GlobalAutodeleteSettings_SetConfirmTitle, text: presentationData.strings.GlobalAutodeleteSettings_SetConfirmText(valueText).string, actions: [ @@ -350,8 +350,8 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, text = presentationData.strings.GlobalAutodeleteSettings_AttemptDisabledGenericSelection } - presentControllerImpl?(standardTextAlertController( - theme: AlertControllerTheme(presentationData: presentationData), + presentControllerImpl?(textAlertController( + context: context, title: nil, text: text, actions: [ diff --git a/submodules/SettingsUI/Sources/Privacy and Security/LoginEmailSetupController.swift b/submodules/SettingsUI/Sources/Privacy and Security/LoginEmailSetupController.swift index 45cb19e099..60087d5419 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/LoginEmailSetupController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/LoginEmailSetupController.swift @@ -5,6 +5,7 @@ import SwiftSignalKit import TelegramCore import AccountContext import TelegramPresentationData +import PresentationDataUtils import AuthorizationUI import AuthenticationServices import UndoUI @@ -76,7 +77,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e var dismissCodeControllerImpl: (() -> Void)? var presentControllerImpl: ((ViewController) -> Void)? - let codeController = AuthorizationSequenceCodeEntryController(presentationData: presentationData, back: { + let codeController = AuthorizationSequenceCodeEntryController(sharedContext: context.sharedContext, presentationData: presentationData, back: { dismissCodeControllerImpl?() dismiss() }) @@ -119,7 +120,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e codeController?.resetCode() } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) } } }, completed: { [weak codeController] in @@ -148,7 +149,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e text = presentationData.strings.Login_EmailNotAllowedError } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) }, completed: { [weak emailController] in emailController?.inProgress = false }) @@ -173,7 +174,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e switch credential { case let appleIdCredential as ASAuthorizationAppleIDCredential: guard let tokenData = appleIdCredential.identityToken, let token = String(data: tokenData, encoding: .utf8) else { - emailController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + emailController?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return } let _ = (verifyLoginEmailChange(account: context.account, code: .appleToken(token)) @@ -193,7 +194,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e case .emailNotAllowed: text = presentationData.strings.Login_EmailNotAllowedError } - emailController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + emailController?.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, completed: { [weak emailController] in emailController?.authorization = nil emailController?.authorizationDelegate = nil diff --git a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift index 333125c412..2317be18fb 100644 --- a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift +++ b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift @@ -6,7 +6,9 @@ import Display import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences +import PresentationDataUtils import ProgressNavigationButtonNode +import AccountContext public class TermsOfServiceControllerTheme { public let statusBarStyle: StatusBarStyle @@ -43,6 +45,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont return self.displayNode as! TermsOfServiceControllerNode } + private let context: AccountContext private let presentationData: PresentationData private let text: String private let entities: [MessageTextEntity] @@ -67,7 +70,8 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont } } - public init(presentationData: PresentationData, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, signingUp: Bool, accept: @escaping (String?) -> Void, decline: @escaping () -> Void, openUrl: @escaping (String) -> Void) { + public init(context: AccountContext, presentationData: PresentationData, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, signingUp: Bool, accept: @escaping (String?) -> Void, decline: @escaping () -> Void, openUrl: @escaping (String) -> Void) { + self.context = context self.presentationData = presentationData self.text = text self.entities = entities @@ -112,7 +116,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont text = strongSelf.presentationData.strings.PrivacyPolicy_DeclineMessage declineTitle = strongSelf.presentationData.strings.PrivacyPolicy_DeclineDeclineAndDelete } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: { + strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: { self?.decline() }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { })], actionLayout: .vertical), in: .window(.root)) @@ -122,7 +126,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont } if let ageConfirmation = strongSelf.ageConfirmation { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationTitle, text: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationMessage("\(ageConfirmation)").string, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationAgree, action: { + strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationTitle, text: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationMessage("\(ageConfirmation)").string, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationAgree, action: { self?.accept(self?.proccessBotNameAfterAccept) })]), in: .window(.root)) } else { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index fc1fac25d5..4cb912c60e 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -10,6 +10,7 @@ import TelegramVoip import TelegramAudio import TelegramUIPreferences import TelegramPresentationData +import PresentationDataUtils import DeviceAccess import UniversalMediaPlayer import AccountContext @@ -2134,12 +2135,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } if case .anonymousNotAllowed = error { let presentationData = self.accountContext.sharedContext.currentPresentationData.with { $0 } - self.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: self.isChannel ? presentationData.strings.LiveStream_AnonymousDisabledAlertText : presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [ + self.accountContext.sharedContext.mainWindow?.present(textAlertController(context: self.accountContext, title: nil, text: self.isChannel ? presentationData.strings.LiveStream_AnonymousDisabledAlertText : presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}) ]), on: .root, blockInteraction: false, completion: {}) } else if case .tooManyParticipants = error { let presentationData = self.accountContext.sharedContext.currentPresentationData.with { $0 } - self.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: self.isChannel ? presentationData.strings.LiveStream_ChatFullAlertText : presentationData.strings.VoiceChat_ChatFullAlertText, actions: [ + self.accountContext.sharedContext.mainWindow?.present(textAlertController(context: self.accountContext, title: nil, text: self.isChannel ? presentationData.strings.LiveStream_ChatFullAlertText : presentationData.strings.VoiceChat_ChatFullAlertText, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}) ]), on: .root, blockInteraction: false, completion: {}) } else if case .invalidJoinAsPeer = error { @@ -3805,7 +3806,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { break } - self.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [ + self.accountContext.sharedContext.mainWindow?.present(textAlertController(context: self.accountContext, title: nil, text: errorText, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}) ]), on: .root, blockInteraction: false, completion: {}) } diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index b613792700..54d828be17 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -1211,8 +1211,7 @@ private final class AdminUserActionsSheetComponent: Component { return } if !isEnabled { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.GroupPermission_PermissionDisabledByDefault, actions: [ + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: environment.strings.GroupPermission_PermissionDisabledByDefault, actions: [ TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: { }) ]), in: .window(.root)) diff --git a/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/BUILD index 0faa548442..97ddeb8af6 100644 --- a/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/BUILD @@ -29,6 +29,8 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/Chat/AccessoryPanelNode", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift index a63706f259..21ed1416f8 100644 --- a/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift @@ -20,6 +20,8 @@ import AnimationCache import MultiAnimationRenderer import AccessoryPanelNode import AppBundle +import ComponentFlow +import AlertComponent func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) { for media in message.media { @@ -378,26 +380,48 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode { string = self.strings.Conversation_ForwardOptions_Text(messages, peerDisplayTitle) } - let font = Font.regular(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0)) - let boldFont = Font.semibold(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0)) - let body = MarkdownAttributeSet(font: font, textColor: self.theme.actionSheet.secondaryTextColor) - let bold = MarkdownAttributeSet(font: boldFont, textColor: self.theme.actionSheet.secondaryTextColor) + let font = Font.regular(15.0) + let boldFont = Font.semibold(15.0) + let body = MarkdownAttributeSet(font: font, textColor: self.theme.actionSheet.primaryTextColor) + let bold = MarkdownAttributeSet(font: boldFont, textColor: self.theme.actionSheet.primaryTextColor) + let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .natural) - let title = NSAttributedString(string: self.strings.Conversation_ForwardOptions_Title(messageCount), font: Font.semibold(floor(self.fontSize.baseDisplaySize)), textColor: self.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .center) + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent( + title: strings.Conversation_ForwardOptions_Title(messageCount) + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .attributed(text)) + ) + )) - let alertController = richTextAlertController(context: self.context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: self.strings.Conversation_ForwardOptions_ShowOptions, action: { [weak self] in - if let strongSelf = self { - strongSelf.interfaceInteraction?.presentForwardOptions(strongSelf.view) - Queue.mainQueue().after(0.5) { - strongSelf.updateThemeAndStrings(theme: strongSelf.theme, strings: strongSelf.strings, forwardOptionsState: strongSelf.forwardOptionsState, force: true) - } - - let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start() - } - }), TextAlertAction(type: .destructiveAction, title: self.strings.Conversation_ForwardOptions_CancelForwarding, action: { [weak self] in - self?.dismiss?() - })], actionLayout: .vertical) + let alertController = AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: strings.Conversation_ForwardOptions_ShowOptions, action: { [weak self] in + guard let self else { + return + } + self.interfaceInteraction?.presentForwardOptions(self.view) + Queue.mainQueue().after(0.5) { + self.updateThemeAndStrings(theme: self.theme, strings: self.strings, forwardOptionsState: self.forwardOptionsState, force: true) + } + let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: self.context.sharedContext.accountManager, count: 3).start() + }), + .init(title: strings.Conversation_ForwardOptions_CancelForwarding, type: .destructive, action: { [weak self] in + self?.dismiss?() + }) + ] + ) self.interfaceInteraction?.presentController(alertController, nil) } diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 7b623f3485..0611f99b02 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -1513,7 +1513,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { case .someUserTooManyChannels: text = presentationData.strings.ChatListFilter_CreateLinkErrorSomeoneHasChannelLimit } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(textAlertController(context: component.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) } }) diff --git a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift index 38b2e173f5..c9fcea3e0a 100644 --- a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift +++ b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift @@ -1095,7 +1095,7 @@ public func notificationPeerExceptionController( let presentationData = context.sharedContext.currentPresentationData.with { $0 } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.PeerInfo_DeleteToneTitle, text: presentationData.strings.PeerInfo_DeleteToneText(title).string, actions: [ + controller.present(textAlertController(context: context, title: presentationData.strings.PeerInfo_DeleteToneTitle, text: presentationData.strings.PeerInfo_DeleteToneText(title).string, actions: [ TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { updateState { state in var state = state diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 0842e3d366..a432b7cef8 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -24,6 +24,7 @@ import PremiumLockButtonSubtitleComponent import ListSectionComponent import ListItemSliderSelectorComponent import ListSwitchItemComponent +import PresentationDataUtils final class PeerAllowedReactionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -202,7 +203,7 @@ final class PeerAllowedReactionsScreenComponent: Component { self.applySettings(standalone: true) } else { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChannelReactions_UnsavedChangesAlertTitle, text: presentationData.strings.ChannelReactions_UnsavedChangesAlertText, actions: [ + self.environment?.controller()?.present(textAlertController(context: component.context, title: presentationData.strings.ChannelReactions_UnsavedChangesAlertTitle, text: presentationData.strings.ChannelReactions_UnsavedChangesAlertText, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.ChannelReactions_UnsavedChangesAlertDiscard, action: { [weak self] in guard let self else { return @@ -306,7 +307,7 @@ final class PeerAllowedReactionsScreenComponent: Component { self.displayPremiumScreen(reactionCount: customReactions.count) case .generic: let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } }, completed: { [weak self] in diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index 8f7831e3b1..73d835958e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -268,8 +268,8 @@ final class AffiliateProgramSetupScreenComponent: Component { return } let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - self.environment?.controller()?.present(standardTextAlertController( - theme: AlertControllerTheme(presentationData: presentationData), + self.environment?.controller()?.present(textAlertController( + context: component.context, title: environment.strings.AffiliateSetup_AlertTerminate_Title, text: environment.strings.AffiliateSetup_AlertTerminate_Text, actions: [ diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 962f534e6b..70fd708ead 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -10265,8 +10265,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } strongSelf.view.endEditing(true) - - strongSelf.controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [ + + strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: title, text: text, actions: [ TextAlertAction(type: .destructiveAction, title: actionText, action: { self?.deletePeerChat(peer: peer._asPeer(), globally: delete) }), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 3f53d93ba1..a7e30d2eb6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -6,6 +6,7 @@ import TelegramCore import SwiftSignalKit import Postbox import TelegramPresentationData +import PresentationDataUtils import AccountContext import ContextUI import PhotoResources @@ -5177,14 +5178,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } public func presentUnableToAddMorePreviewsAlert() { - self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.BotPreviews_AlertTooManyPreviews(Int32(self.maxBotPreviewCount)), actions: [ + self.parentController?.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.BotPreviews_AlertTooManyPreviews(Int32(self.maxBotPreviewCount)), actions: [ TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { }) ], parseMarkdown: true), in: .window(.root)) } public func presentDeleteBotPreviewLanguage() { - self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Title, text: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Text, actions: [ + self.parentController?.present(textAlertController(context: self.context, title: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Title, text: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Text, actions: [ TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index 9a4d2879b4..5a30131d3f 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -195,7 +195,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { if self.isOn { if !self.hasAccessToAllChatsByDefault && self.additionalPeerList.categories.isEmpty && self.additionalPeerList.peers.isEmpty { - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.BusinessMessageSetup_ErrorNoRecipients_Text, actions: [ + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: presentationData.strings.BusinessMessageSetup_ErrorNoRecipients_Text, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: presentationData.strings.BusinessMessageSetup_ErrorNoRecipients_ResetAction, action: { @@ -223,7 +223,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { } if let errorText { - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [ + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: errorText, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: presentationData.strings.BusinessMessageSetup_ErrorScheduleTime_ResetAction, action: { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift index 6eef6691bb..d7d1e46641 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift @@ -4,6 +4,7 @@ import Display import ComponentFlow import ListSectionComponent import TelegramPresentationData +import PresentationDataUtils import AppBundle import AccountContext import ViewControllerComponent @@ -185,7 +186,7 @@ final class BusinessLinksSetupScreenComponent: Component { errorText = presentationData.strings.Business_Links_ErrorTooManyLinks } - environment.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [ + environment.controller()?.present(textAlertController(context: component.context, title: nil, text: errorText, actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { }) ]), in: .window(.root)) diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift index a5dee4feb1..6e90d95f50 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -127,8 +127,7 @@ final class BusinessDaySetupScreenComponent: Component { } } - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: enviroment.strings.BusinessHoursSetup_ErrorIntersectingHours_Text, actions: [ + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: enviroment.strings.BusinessHoursSetup_ErrorIntersectingHours_Text, actions: [ TextAlertAction(type: .genericAction, title: enviroment.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: enviroment.strings.BusinessHoursSetup_ErrorIntersectingHours_ResetAction, action: { diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift index 5b453f54f7..71e325ae1a 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift @@ -307,8 +307,7 @@ final class BusinessHoursSetupScreenComponent: Component { let _ = component.context.engine.accountData.updateAccountBusinessHours(businessHours: businessHours).startStandalone() return true } catch _ { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessHoursSetup_ErrorIntersectingDays_Text, actions: [ + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: environment.strings.BusinessHoursSetup_ErrorIntersectingDays_Text, actions: [ TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: environment.strings.BusinessHoursSetup_ErrorIntersectingDays_ResetAction, action: { [weak self] in diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index f1836fed75..6f0654f867 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -127,8 +127,7 @@ final class BusinessLocationSetupScreenComponent: Component { let businessLocation = self.currentBusinessLocation() if businessLocation != component.initialValue { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_Text, actions: [ + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_Text, actions: [ TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_ResetAction, action: { @@ -250,8 +249,7 @@ final class BusinessLocationSetupScreenComponent: Component { let businessLocation = self.currentBusinessLocation() if businessLocation != nil && address.isEmpty { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_Text, actions: [ + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_Text, actions: [ TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: { }) ]), in: .window(.root)) diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 38e5a4afd8..0a8988b6c7 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -792,7 +792,7 @@ final class ChatbotSetupScreenComponent: Component { self.botRights = [.reply, .readMessages, .deleteSentMessages, .deleteReceivedMessages] self.state?.updated(transition: .spring(duration: 0.3)) } else { - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.ChatbotSetup_ErrorBotNotBusinessCapable, actions: [ + self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: presentationData.strings.ChatbotSetup_ErrorBotNotBusinessCapable, actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { }) ]), in: .window(.root)) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 652d91f344..619f13e4b1 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -760,7 +760,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let absMaxEmojiCount = paramSets.paramSets.max(by: { $0.minStars < $1.minStars })?.maxEmojiCount ?? 10 if emojiCount > absMaxEmojiCount { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - view.component?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.LiveStream_ErrorMaxAllowedEmoji_Text(Int32(absMaxEmojiCount)), actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + view.component?.controller()?.present(textAlertController(context: component.context, updatedPresentationData: (presentationData, .single(presentationData)), title: nil, text: presentationData.strings.LiveStream_ErrorMaxAllowedEmoji_Text(Int32(absMaxEmojiCount)), actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return } @@ -2409,18 +2409,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { component.controller()?.push(controller) } }, presentSelectionLimitExceeded: { [weak view] in - guard let view else { + guard let view, let component = view.component else { return } - let text: String if slowModeEnabled { text = presentationData.strings.Chat_SlowmodeAttachmentLimitReached } else { text = presentationData.strings.Chat_AttachmentLimitReached } - - view.component?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + component.controller()?.present(textAlertController(context: component.context, updatedPresentationData: (presentationData, .single(presentationData)), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentSchedulePicker: { [weak view] media, done in if let strongSelf = self, let view { strongSelf.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod in diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 8953143641..99b50385b0 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2422,7 +2422,7 @@ private func extractAccountManagerState(records: AccountRecordsView Void)? var declineImpl: (() -> Void)? - let controller = TermsOfServiceController(presentationData: presentationData, text: termsOfServiceUpdate.text, entities: termsOfServiceUpdate.entities, ageConfirmation: termsOfServiceUpdate.ageConfirmation, signingUp: false, accept: { proccedBot in + let controller = TermsOfServiceController(context: strongSelf.context, presentationData: presentationData, text: termsOfServiceUpdate.text, entities: termsOfServiceUpdate.entities, ageConfirmation: termsOfServiceUpdate.ageConfirmation, signingUp: false, accept: { proccedBot in acceptImpl?(proccedBot) }, decline: { declineImpl?() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index 7c213a885b..9358698e53 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -18,6 +18,7 @@ import Display import Markdown import TextFormat import TelegramPresentationData +import AlertComponent func textInputAccessoryPanel( context: AccountContext, @@ -112,7 +113,6 @@ func textInputAccessoryPanel( let theme = chatPresentationInterfaceState.theme let strings = chatPresentationInterfaceState.strings let nameDisplayOrder = chatPresentationInterfaceState.nameDisplayOrder - let fontSize = chatPresentationInterfaceState.fontSize return AnyComponentWithIdentity(id: "forward", component: AnyComponent(ChatInputMessageAccessoryPanel( context: context, @@ -158,23 +158,45 @@ func textInputAccessoryPanel( string = strings.Conversation_ForwardOptions_Text(messages, peerDisplayTitle) } - let font = Font.regular(floor(fontSize.baseDisplaySize * 15.0 / 17.0)) - let boldFont = Font.semibold(floor(fontSize.baseDisplaySize * 15.0 / 17.0)) - let body = MarkdownAttributeSet(font: font, textColor: theme.actionSheet.secondaryTextColor) - let bold = MarkdownAttributeSet(font: boldFont, textColor: theme.actionSheet.secondaryTextColor) + let font = Font.regular(15.0) + let boldFont = Font.semibold(15.0) + let body = MarkdownAttributeSet(font: font, textColor: theme.actionSheet.primaryTextColor) + let bold = MarkdownAttributeSet(font: boldFont, textColor: theme.actionSheet.primaryTextColor) + let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .natural) - let title = NSAttributedString(string: strings.Conversation_ForwardOptions_Title(messageCount), font: Font.semibold(floor(fontSize.baseDisplaySize)), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .center) + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent( + title: strings.Conversation_ForwardOptions_Title(messageCount) + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .attributed(text)) + ) + )) - let alertController = richTextAlertController(context: context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: strings.Conversation_ForwardOptions_ShowOptions, action: { - guard let sourceView else { - return - } - interfaceInteraction?.presentForwardOptions(sourceView) - let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: context.sharedContext.accountManager, count: 3).start() - }), TextAlertAction(type: .destructiveAction, title: strings.Conversation_ForwardOptions_CancelForwarding, action: { - interfaceInteraction?.dismissForwardMessages() - })], actionLayout: .vertical) + let alertController = AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: strings.Conversation_ForwardOptions_ShowOptions, action: { + guard let sourceView else { + return + } + interfaceInteraction?.presentForwardOptions(sourceView) + let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: context.sharedContext.accountManager, count: 3).start() + }), + .init(title: strings.Conversation_ForwardOptions_CancelForwarding, type: .destructive, action: { + interfaceInteraction?.dismissForwardMessages() + }) + ] + ) interfaceInteraction?.presentController(alertController, nil) } } From f7f3b92fa7553819be66b0ad9bddf2ea61a455ee Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 23 Dec 2025 22:25:44 +0400 Subject: [PATCH 3/6] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 15 + .../Sources/AccountContext.swift | 2 +- .../Sources/AttachmentPanel.swift | 4 +- .../Sources/ChatTextLinkEditController.swift | 31 +- .../ContextControllerActionsStackNode.swift | 8 +- .../PromptUI/Sources/PromptController.swift | 273 ------ submodules/SettingsUI/BUILD | 1 + .../WebBrowserDomainController.swift | 529 ++--------- .../WebBrowserSettingsController.swift | 2 +- submodules/TelegramCallsUI/BUILD | 3 +- .../Components/MediaStreamComponent.swift | 4 +- .../Sources/VideoChatScreen.swift | 2 +- .../Sources/VideoChatScreenMoreMenu.swift | 2 +- ...ideoChatScreenParticipantContextMenu.swift | 4 +- .../VoiceChatTitleEditController.swift | 884 +++--------------- submodules/TelegramUI/BUILD | 1 + .../AlertInputFieldComponent/BUILD | 28 + .../Sources/AlertInputFieldComponent.swift | 328 +++++++ .../AlertMultilineInputFieldComponent.swift | 25 +- .../Sources/AlertActionComponent.swift | 18 +- .../Sources/AlertComponent.swift | 2 +- .../Components/CocoonInfoScreen/BUILD | 37 + .../Sources/CocoonInfoScreen.swift | 669 +++++++++++++ .../EntityKeyboardBottomPanelComponent.swift | 4 +- .../GiftAuctionCustomBidController.swift | 590 ++++++------ .../Sources/GiftViewScreen.swift | 31 +- .../Sources/GlobalControlPanelsContext.swift | 17 + .../Sources/AccountFreezeInfoScreen.swift | 49 +- .../BusinessLinkNameAlertController/BUILD | 2 + .../BusinessLinkNameAlertController.swift | 584 ++---------- .../QuickReplyNameAlertController/BUILD | 2 + .../QuickReplyNameAlertController.swift | 96 ++ .../Sources/StarsTransactionScreen.swift | 2 +- .../Premium/Cocoon.imageset/Contents.json | 21 + .../Premium/Cocoon.imageset/cocoon.png | Bin 0 -> 101385 bytes .../Contents.json | 0 .../wear_30 (2).pdf | Bin .../Chat/ChatControllerLoadDisplayNode.swift | 12 + .../Sources/ChatControllerEditChat.swift | 9 +- .../Sources/SharedAccountContext.swift | 5 + 40 files changed, 1884 insertions(+), 2412 deletions(-) create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/BUILD create mode 100644 submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift create mode 100644 submodules/TelegramUI/Components/CocoonInfoScreen/BUILD create mode 100644 submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/cocoon.png rename submodules/TelegramUI/Images.xcassets/Premium/Collectible/{Wearing.imageset => Wearable.imageset}/Contents.json (100%) rename submodules/TelegramUI/Images.xcassets/Premium/Collectible/{Wearing.imageset => Wearable.imageset}/wear_30 (2).pdf (100%) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 8e091cb4ba..95f37268bc 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15633,3 +15633,18 @@ Error: %8$@"; "MediaEditor.Audio.ShowMore" = "Show More"; "WebApp.AddToAttachmentTitle" = "Add to Attachment Menu"; + +"Gift.Upgrade.Wearable.Title" = "Wearable"; +"Gift.Upgrade.Wearable.Text" = "Display gifts on your page and set them as profile covers or statuses."; +"Gift.Upgrade.ViewAllVariants" = "View all variants"; + +"CocoonInfo.Title" = "COCOON"; +"CocoonInfo.Description" = "**Cocoon** (**Co**nfidential **Co**mpute **O**pen **N**etwork) handles AI tasks **safely** and **efficiently**."; +"CocoonInfo.Private.Title" = "Private"; +"CocoonInfo.Private.Text" = "No third party can access any data, such as translations, inside Cocoon."; +"CocoonInfo.Efficient.Title" = "Efficient"; +"CocoonInfo.Efficient.Text" = "Cocoon has allowed Telegram to reduce translation costs by 6x."; +"CocoonInfo.ForEveryone.Title" = "For Everyone"; +"CocoonInfo.ForEveryone.Text" = "Any developer can use Cocoon for AI features. Learn more at [@cocoon](telegram) or [cocoon.org](web)."; +"CocoonInfo.IntergrateInfo" = "Want to integrate Cocoon into your projects?\nReach out at [t.me/cocoon?direct]()"; +"CocoonInfo.Understood" = "Understood"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 978c9b3ecb..7d382b4a8d 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1428,7 +1428,7 @@ public protocol SharedAccountContext: AnyObject { func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController - + func makeCocoonInfoScreen(context: AccountContext) -> ViewController func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController @available(iOS 13.0, *) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 2a102a4cf5..beb821f279 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1517,7 +1517,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { switch recognizer.state { case .began: if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { - let startX = itemView.frame.minX - 4.0 + let startX = itemView.frame.minX self.selectionGestureState = (startX, startX, itemId) self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) @@ -2077,7 +2077,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { 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 backgroundOriginX: CGFloat = isSelecting ? panelSideInset : floorToScreenPixels((layout.size.width - panelSize.width) / 2.0) - liquidLensView.update(size: panelSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), inset: 0.0, isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition)) + liquidLensView.update(size: CGSize(width: panelSize.width, height: panelSize.height), selectionOrigin: CGPoint(x: max(0.0, min(panelSize.width - lensSelection.width, lensSelection.x)), y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), inset: 3.0, isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition)) transition.updatePosition(layer: liquidLensView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5)) transition.updateBounds(layer: liquidLensView.layer, bounds: CGRect(origin: .zero, size: panelSize)) diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift index 51996c3d55..4229f74605 100644 --- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift +++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift @@ -36,6 +36,8 @@ public func chatTextLinkEditController( AlertTextComponent(content: .plain(strings.TextFormat_AddLinkText(text).string)) ) )) + + var applyImpl: (() -> Void)? content.append(AnyComponentWithIdentity( id: "input", component: AnyComponent( @@ -43,11 +45,15 @@ public func chatTextLinkEditController( context: context, initialValue: link.flatMap { NSAttributedString(string: $0) }, placeholder: strings.TextFormat_AddLinkPlaceholder, + returnKeyType: .done, keyboardType: .URL, autocapitalizationType: .none, autocorrectionType: .no, isInitiallyFocused: true, - externalState: inputState + externalState: inputState, + returnKeyAction: { + applyImpl?() + } ) ) )) @@ -66,20 +72,23 @@ public func chatTextLinkEditController( actions: [ .init(title: strings.Common_Cancel), .init(title: strings.Common_Done, type: .default, action: { - let updatedLink = explicitUrl(inputState.value.string) - if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) { - dismissImpl?() - apply(updatedLink) - } else if inputState.value.string.isEmpty { - dismissImpl?() - apply("") - } else { - inputState.animateError() - } + applyImpl?() }, autoDismiss: false) ], updatedPresentationData: effectiveUpdatedPresentationData ) + applyImpl = { + let updatedLink = explicitUrl(inputState.value.string) + if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) { + dismissImpl?() + apply(updatedLink) + } else if inputState.value.string.isEmpty { + dismissImpl?() + apply("") + } else { + inputState.animateError() + } + } dismissImpl = { [weak alertController] in alertController?.dismiss(completion: nil) } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index dbf823676a..2bb3470ff8 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -346,6 +346,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking } let titleColor: UIColor + let linkColor = presentationData.theme.list.itemAccentColor switch self.item.textColor { case .primary: titleColor = presentationData.theme.contextMenu.primaryColor @@ -365,6 +366,8 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking return ChatTextInputStateTextAttribute(type: .bold, range: entity.range) } else if case .Italic = entity.type { return ChatTextInputStateTextAttribute(type: .italic, range: entity.range) + } else if case .Url = entity.type { + return ChatTextInputStateTextAttribute(type: .textUrl(""), range: entity.range) } return nil }) @@ -375,9 +378,12 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking ], range: NSRange(location: 0, length: result.length)) for attribute in inputStateText.attributes { if case .bold = attribute.type { - result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(presentationData.listsFontSize.baseDisplaySize), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + result.addAttribute(NSAttributedString.Key.font, value: titleBoldFont, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) } else if case .italic = attribute.type { result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + } else if case .textUrl = attribute.type { + result.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + result.addAttribute(NSAttributedString.Key.font, value: titleBoldFont, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) } } attributedText = result diff --git a/submodules/PromptUI/Sources/PromptController.swift b/submodules/PromptUI/Sources/PromptController.swift index 02ac8f6572..d359be83de 100644 --- a/submodules/PromptUI/Sources/PromptController.swift +++ b/submodules/PromptUI/Sources/PromptController.swift @@ -197,279 +197,6 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg } } -private final class PromptAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let text: String - private let titleFont: PromptControllerTitleFont - private let subtitle: String? - - private let textNode: ASTextNode - private let subtitleNode: ASTextNode? - let inputFieldNode: PromptInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - self.inputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, titleFont: PromptControllerTitleFont, subtitle: String?, value: String?, placeholder: String?, characterLimit: Int, displayCharacterLimit: Bool) { - self.strings = strings - self.text = text - self.titleFont = titleFont - self.subtitle = subtitle - - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 2 - - if subtitle != nil { - let subtitleNode = ASTextNode() - subtitleNode.maximumNumberOfLines = 0 - self.subtitleNode = subtitleNode - } else { - self.subtitleNode = nil - } - - self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: placeholder ?? "", characterLimit: characterLimit, displayCharacterLimit: displayCharacterLimit) - self.inputFieldNode.text = value ?? "" - - 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.textNode) - if let subtitleNode = self.subtitleNode { - self.addSubnode(subtitleNode) - } - - self.addSubnode(self.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - self.actionNodes.last?.actionEnabled = true - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) - } - } - } - -// self.inputFieldNode.textChanged = { [weak self] text in -// if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { -// lastNode.actionEnabled = !text.isEmpty -// } -// } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - var value: String { - return self.inputFieldNode.text - } - - override func updateTheme(_ theme: AlertControllerTheme) { - let titleFontValue: UIFont - switch self.titleFont { - case .regular: - titleFontValue = Font.regular(13.0) - case .bold: - titleFontValue = Font.semibold(17.0) - } - self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFontValue, textColor: theme.primaryColor, paragraphAlignment: .center) - - if let subtitle = self.subtitle, let subtitleNode = self.subtitleNode { - subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - } - - 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) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 5.0 - - let titleSize = CGSize() -// 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(measureSize) - 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 + 6.0 + spacing - - var subtitleSize: CGSize? - if let subtitleNode { - let subtitleSizeValue = subtitleNode.measure(measureSize) - subtitleSize = subtitleSizeValue - transition.updateFrame(node: subtitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - subtitleSizeValue.width) / 2.0), y: origin.y), size: subtitleSizeValue)) - origin.y += subtitleSizeValue.height + 6.0 + spacing - } - - 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: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - if let subtitleSize { - contentWidth = max(contentWidth, subtitleSize.width) - } - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - var resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) - if let subtitleSize { - resultSize.height += subtitleSize.height + spacing - } - - 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 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() - } -} - public enum PromptControllerTitleFont { case regular case bold diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index f19a07ca6f..6aacf354f0 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -132,6 +132,7 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/SliderComponent", "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift index 4d62514a07..88a33bcdba 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift @@ -7,465 +7,100 @@ import TelegramCore import TelegramPresentationData import AccountContext import UrlEscaping -import ActivityIndicator +import ComponentFlow +import AlertComponent +import AlertInputFieldComponent -private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - fileprivate let textInputNode: EditableTextNode - private let placeholderNode: ASTextNode +public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, apply: @escaping (String?) -> Void) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? + let inputState = AlertInputFieldComponent.ExternalState() - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) - - var text: String { - get { - return self.textInputNode.attributedText?.string ?? "" - } - set { - self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) - self.placeholderNode.isHidden = !newValue.isEmpty - } + let doneIsEnabled: Signal = inputState.valueSignal + |> map { value in + return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - init(theme: PresentationTheme, placeholder: String) { - self.theme = theme + let doneInProgressValuePromise = ValuePromise(false) + let doneInProgress = doneInProgressValuePromise.get() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.WebBrowser_Exceptions_Create_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.WebBrowser_Exceptions_Create_Text)) + ) + )) - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = EditableTextNode() - self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] - self.textInputNode.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) - self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.keyboardType = .URL - self.textInputNode.autocapitalizationType = .none - self.textInputNode.returnKeyType = .done - self.textInputNode.autocorrectionType = .no - self.textInputNode.tintColor = theme.actionSheet.controlAccentColor - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - super.init() - - self.textInputNode.delegate = self - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.updateTextNodeText(animated: true) - self.textChanged?(editableTextNode.textView.text) - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty - } - - private let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: []) - private let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: []) - - var inProgress = false - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if self.inProgress { - return false - } - if text == "\n" { - self.complete?() - return false - } - - if let domainRegex = self.domainRegex, let pathRegex = self.pathRegex { - let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) - let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) - let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) - - if domainMatches.count > 0, pathMatches.count == 0 { - return true - } else { - return false - } - } - - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(33.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.textInputNode.attributedText = nil - self.deactivateInput() - } -} - -private final class WebBrowserDomainAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - - private let titleNode: ASTextNode - private let textNode: ASTextNode - let activityIndicator: ActivityIndicator - let inputFieldNode: WebBrowserDomainInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - self.inputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) { - self.strings = strings - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 2 - - self.activityIndicator = ActivityIndicator(type: .custom(ptheme.rootController.navigationBar.secondaryTextColor, 20.0, 1.5, false), speed: .slow) - self.activityIndicator.isHidden = true - - self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder) - self.inputFieldNode.text = "" - - 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.inputFieldNode) - self.addSubnode(self.activityIndicator) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - self.actionNodes.last?.actionEnabled = false - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) - } - } - } - - self.inputFieldNode.textChanged = { [weak self] text in - if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { - lastNode.actionEnabled = !text.isEmpty - } - } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - var link: String { - return self.inputFieldNode.text - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - 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) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 5.0 - - let titleSize = self.titleNode.measure(measureSize) - 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(measureSize) - 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 + 6.0 + spacing - - 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: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - let inputFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight) - transition.updateFrame(node: self.inputFieldNode, frame: inputFrame) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - let activitySize = CGSize(width: 20.0, height: 20.0) - transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: inputFrame.maxX - activitySize.width - 23.0, y: inputFrame.midY - activitySize.height / 2.0 - 3.0), size: activitySize)) - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + 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 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() - } -} - -public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, apply: @escaping (String?) -> Void) -> AlertController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - - var dismissImpl: ((Bool) -> Void)? + let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: []) + let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: []) var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: nil, + placeholder: strings.WebBrowser_Exceptions_Create_Placeholder, + characterLimit: nil, + hasClearButton: true, + keyboardType: .URL, + autocapitalizationType: .none, + autocorrectionType: .no, + isInitiallyFocused: true, + externalState: inputState, + shouldChangeText: { updatedText in + guard let domainRegex, let pathRegex else { + return true + } + let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) + let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) + if domainMatches.count > 0, pathMatches.count == 0 { + return true + } else { + return false + } + }, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) - var inProgress = false - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - if !inProgress { - dismissImpl?(true) - apply(nil) - } - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { - if !inProgress { - applyImpl?() - } - })] - - let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions) - contentNode.complete = { - applyImpl?() + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - inProgress = true - contentNode.inputFieldNode.inProgress = true - contentNode.activityIndicator.isHidden = false - - let updatedLink = explicitUrl(contentNode.link) + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Common_Done, type: .default, action: { + applyImpl?() + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgress) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + let updatedLink = explicitUrl(inputState.value) if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) { + doneInProgressValuePromise.set(true) apply(updatedLink) } else { - contentNode.animateError() + inputState.animateError() } } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller] animated in - contentNode.inputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + return alertController } diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index c66c0a9916..85fd21740e 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -419,7 +419,7 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl }) dismissImpl = { [weak linkController] in linkController?.view.endEditing(true) - linkController?.dismissAnimated() + linkController?.dismiss(completion: nil) } controller?.present(linkController, in: .window(.root)) } diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index bad899d08f..c771b6f492 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -115,7 +115,6 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/BackButtonComponent", - "//submodules/TelegramUI/Components/AlertComponent", "//submodules/Components/BlurredBackgroundComponent", "//submodules/DirectMediaImageCache", "//submodules/FastBlur", @@ -131,6 +130,8 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/PremiumUI", "//submodules/TelegramUI/Components/Calls/VideoChatMicButtonComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 5edc8d2731..3a62b8af02 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -508,7 +508,7 @@ public final class MediaStreamComponent: CombinedComponent { let title: String = presentationData.strings.LiveStream_EditTitle let text: String = presentationData.strings.LiveStream_EditTitleText - let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in + let editController = voiceChatTitleEditController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in guard let call = call else { return } @@ -574,7 +574,7 @@ public final class MediaStreamComponent: CombinedComponent { title = presentationData.strings.LiveStream_StartRecordingTitle text = presentationData.strings.LiveStream_StartRecordingTextVideo - let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in + let editController = voiceChatTitleEditController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in guard let call = call, let controller = controller else { return } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index e6b2071969..fcf96a12fb 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -691,7 +691,7 @@ final class VideoChatScreenComponent: Component { text = environment.strings.VoiceChat_EditTitleText } - let controller = voiceChatTitleEditController(sharedContext: groupCall.accountContext.sharedContext, account: groupCall.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in + let controller = voiceChatTitleEditController(context: groupCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index e77e72bf50..fed8bd66dc 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -469,7 +469,7 @@ extension VideoChatScreenComponent.View { } } - let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in + let controller = voiceChatTitleEditController(context: currentCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall, let peer = self.peer, let title else { return } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index dff33a3bfa..3b03188797 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -117,7 +117,7 @@ extension VideoChatScreenComponent.View { } else { maxBioLength = 100 } - let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in + let controller = voiceChatTitleEditController(context: currentCall.accountContext, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in guard let self, let environment = self.environment, let currentCall = self.currentCall, let bio else { return } @@ -149,7 +149,7 @@ extension VideoChatScreenComponent.View { guard let self, let environment = self.environment, let currentCall = self.currentCall else { return } - let controller = voiceChatUserNameController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in + let controller = voiceChatUserNameController(context: currentCall.accountContext, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in guard let self, let environment = self.environment, let currentCall = self.currentCall, let (firstName, lastName) = firstAndLastName else { return } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift index 1ba276b3db..505fb7d28a 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift @@ -7,767 +7,179 @@ import TelegramCore import TelegramPresentationData import AccountContext import UrlEscaping +import ComponentFlow +import AlertComponent +import AlertInputFieldComponent -private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - private let textInputNode: EditableTextNode - private let placeholderNode: ASTextNode - private let clearButton: HighlightableButtonNode - - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? - - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) - - var text: String { - get { - return self.textInputNode.attributedText?.string ?? "" - } - set { - self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) - self.placeholderNode.isHidden = !newValue.isEmpty - if self.textInputNode.isFirstResponder() { - self.clearButton.isHidden = newValue.isEmpty - } else { - self.clearButton.isHidden = true - } - } - } - - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - private let maxLength: Int - - init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) { - self.theme = theme - self.maxLength = maxLength - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = EditableTextNode() - self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] - self.textInputNode.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) - self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.keyboardType = .default - self.textInputNode.autocapitalizationType = .sentences - self.textInputNode.returnKeyType = returnKeyType - self.textInputNode.autocorrectionType = .default - self.textInputNode.tintColor = theme.actionSheet.controlAccentColor - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - self.clearButton = HighlightableButtonNode() - self.clearButton.imageNode.displaysAsynchronously = false - self.clearButton.imageNode.displayWithoutProcessing = true - self.clearButton.displaysAsynchronously = false - self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) - self.clearButton.isHidden = true - - super.init() - - self.textInputNode.delegate = self - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - self.addSubnode(self.clearButton) - - self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor - self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height))) - - if let image = self.clearButton.image(for: []) { - transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size)) - } - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.updateTextNodeText(animated: true) - self.textChanged?(editableTextNode.textView.text) - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty - self.clearButton.isHidden = !self.placeholderNode.isHidden - } - - func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.clearButton.isHidden = (editableTextNode.textView.text ?? "").isEmpty - } - - func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.clearButton.isHidden = true - } - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) - if updatedText.count > maxLength { - self.textInputNode.layer.addShakeAnimation() - return false - } - if text == "\n" { - self.complete?() - return false - } - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - 20.0, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(33.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.placeholderNode.isHidden = false - self.clearButton.isHidden = true - - self.textInputNode.attributedText = nil - self.updateHeight?() - } -} - -private final class VoiceChatTitleEditAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let title: String - private let text: String - - private let titleNode: ASTextNode - private let textNode: ASTextNode - let inputFieldNode: VoiceChatTitleEditInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - self.inputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?, maxLength: Int) { - self.strings = strings - self.title = title - self.text = text - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 8 - - self.inputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: placeholder, maxLength: maxLength) - self.inputFieldNode.text = value ?? "" - - 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.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) - } - } - } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - var value: String { - return self.inputFieldNode.text - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - 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) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 5.0 - - let titleSize = self.titleNode.measure(measureSize) - 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(measureSize) - 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 + 6.0 + spacing - - 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: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + 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 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() - } -} - -func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, maxLength: Int, apply: @escaping (String?) -> Void) -> AlertController { - var presentationData = sharedContext.currentPresentationData.with { $0 } - if let forceTheme = forceTheme { +func voiceChatTitleEditController( + context: AccountContext, + forceTheme: PresentationTheme?, + title: String, + text: String, + placeholder: String, + doneButtonTitle: String? = nil, + value: String?, + maxLength: Int, + apply: @escaping (String?) -> Void +) -> ViewController { + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } - - var dismissImpl: ((Bool) -> Void)? + let strings = presentationData.strings + + let inputState = AlertInputFieldComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + var applyImpl: (() -> Void)? - - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: { - applyImpl?() - })] - - let contentNode = VoiceChatTitleEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value, maxLength: maxLength) - contentNode.complete = { - applyImpl?() - } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - dismissImpl?(true) - + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: value, + placeholder: placeholder, + characterLimit: maxLength, + hasClearButton: true, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: doneButtonTitle ?? strings.Common_Done, type: .default, action: { + applyImpl?() + }) + ], + updatedPresentationData: (presentationData, .single(presentationData)) + ) + applyImpl = { let previousValue = value ?? "" - let newValue = contentNode.value.trimmingCharacters(in: .whitespacesAndNewlines) + let newValue = inputState.value.trimmingCharacters(in: .whitespacesAndNewlines) apply(previousValue != newValue || value == nil ? newValue : nil) } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in - var presentationData = presentationData - if let forceTheme = forceTheme { - presentationData = presentationData.withUpdated(theme: forceTheme) - } - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller, weak contentNode] animated in - contentNode?.inputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + return alertController } -private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let title: String - - private let titleNode: ASTextNode - let firstNameInputFieldNode: VoiceChatTitleEditInputFieldNode - let lastNameInputFieldNode: VoiceChatTitleEditInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - self.lastNameInputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, firstNamePlaceholder: String, lastNamePlaceholder: String, firstNameValue: String?, lastNameValue: String?, maxLength: Int) { - self.strings = strings - self.title = title - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - - self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength, returnKeyType: .next) - self.firstNameInputFieldNode.text = firstNameValue ?? "" - - self.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength) - self.lastNameInputFieldNode.text = lastNameValue ?? "" - - 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.firstNameInputFieldNode) - self.addSubnode(self.lastNameInputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - - self.firstNameInputFieldNode.complete = { [weak self] in - self?.lastNameInputFieldNode.activateInput() - } - } - - deinit { - self.disposable.dispose() - } - - var firstName: String { - return self.firstNameInputFieldNode.text - } - - var lastName: String { - return self.lastNameInputFieldNode.text - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - 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) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 0.0 - - let titleSize = self.titleNode.measure(measureSize) - 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 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: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let firstInputFieldHeight = self.firstNameInputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - transition.updateFrame(node: self.firstNameInputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: firstInputFieldHeight)) - - origin.y += firstInputFieldHeight + spacing - - let lastInputFieldHeight = self.lastNameInputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - transition.updateFrame(node: self.lastNameInputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: lastInputFieldHeight)) - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + firstInputFieldHeight + spacing + lastInputFieldHeight + actionsHeight + 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 - } - - if !hadValidLayout { - self.firstNameInputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - if self.firstNameInputFieldNode.text.isEmpty { - self.firstNameInputFieldNode.layer.addShakeAnimation() - } - self.hapticFeedback.error() - } -} - -func voiceChatUserNameController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, firstNamePlaceholder: String, lastNamePlaceholder: String, doneButtonTitle: String? = nil, firstName: String?, lastName: String?, maxLength: Int, apply: @escaping ((String, String)?) -> Void) -> AlertController { - var presentationData = sharedContext.currentPresentationData.with { $0 } - if let forceTheme = forceTheme { +func voiceChatUserNameController( + context: AccountContext, + forceTheme: PresentationTheme?, + title: String, + firstNamePlaceholder: String, + lastNamePlaceholder: String, + doneButtonTitle: String? = nil, + firstName: String?, + lastName: String?, + maxLength: Int, + apply: @escaping ((String, String)?) -> Void +) -> ViewController { + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } + let strings = presentationData.strings + + let firstNameState = AlertInputFieldComponent.ExternalState() + let lastNameState = AlertInputFieldComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + )) - var dismissImpl: ((Bool) -> Void)? + var nextImpl: (() -> Void)? var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "firstName", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: firstName, + placeholder: firstNamePlaceholder, + characterLimit: maxLength, + hasClearButton: true, + returnKeyType: .next, + isInitiallyFocused: true, + externalState: firstNameState, + returnKeyAction: { + nextImpl?() + } + ) + ) + )) - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: { - applyImpl?() - })] - - let contentNode = VoiceChatUserNameEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, firstNamePlaceholder: firstNamePlaceholder, lastNamePlaceholder: lastNamePlaceholder, firstNameValue: firstName, lastNameValue: lastName, maxLength: maxLength) - contentNode.complete = { - applyImpl?() + content.append(AnyComponentWithIdentity( + id: "lastName", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: lastName, + placeholder: lastNamePlaceholder, + characterLimit: maxLength, + hasClearButton: true, + isInitiallyFocused: false, + externalState: lastNameState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: doneButtonTitle ?? strings.Common_Done, type: .default, action: { + applyImpl?() + }) + ], + updatedPresentationData: (presentationData, .single(presentationData)) + ) + nextImpl = { + lastNameState.activateInput() } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - + applyImpl = { let previousFirstName = firstName ?? "" let previousLastName = lastName ?? "" - let newFirstName = contentNode.firstName.trimmingCharacters(in: .whitespacesAndNewlines) - let newLastName = contentNode.lastName.trimmingCharacters(in: .whitespacesAndNewlines) + let newFirstName = firstNameState.value.trimmingCharacters(in: .whitespacesAndNewlines) + let newLastName = lastNameState.value.trimmingCharacters(in: .whitespacesAndNewlines) if newFirstName.isEmpty { - contentNode.animateError() + firstNameState.animateError() return } - - dismissImpl?(true) - + if previousFirstName != newFirstName || previousLastName != newLastName { apply((newFirstName, newLastName)) } else { apply(nil) } } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in - var presentationData = presentationData - if let forceTheme = forceTheme { - presentationData = presentationData.withUpdated(theme: forceTheme) - } - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.firstNameInputFieldNode.updateTheme(presentationData.theme) - contentNode?.lastNameInputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller, weak contentNode] animated in - contentNode?.firstNameInputFieldNode.deactivateInput() - contentNode?.lastNameInputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + return alertController } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 083f6d1a3b..f413eb739b 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -511,6 +511,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiGameStakeScreen", "//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController", + "//submodules/TelegramUI/Components/CocoonInfoScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/BUILD b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/BUILD new file mode 100644 index 0000000000..86dafdff82 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AlertInputFieldComponent", + module_name = "AlertInputFieldComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift new file mode 100644 index 0000000000..902c0d20ff --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift @@ -0,0 +1,328 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AlertComponent +import MultilineTextComponent +import AccountContext +import TextFormat +import PlainButtonComponent +import BundleIconComponent + +public final class AlertInputFieldComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public class ExternalState { + public fileprivate(set) var value: String = "" + public fileprivate(set) var animateError: () -> Void = {} + public fileprivate(set) var activateInput: () -> Void = {} + fileprivate let valuePromise = ValuePromise("") + public var valueSignal: Signal { + return self.valuePromise.get() + } + + public init() { + } + } + + let context: AccountContext + let initialValue: String? + let placeholder: String + let characterLimit: Int? + let hasClearButton: Bool + let isSecureTextEntry: Bool + let returnKeyType: UIReturnKeyType + let keyboardType: UIKeyboardType + let autocapitalizationType: UITextAutocapitalizationType + let autocorrectionType: UITextAutocorrectionType + let isInitiallyFocused: Bool + let externalState: ExternalState + let shouldChangeText: ((String) -> Bool)? + let returnKeyAction: (() -> Void)? + + public init( + context: AccountContext, + initialValue: String? = nil, + placeholder: String, + characterLimit: Int? = nil, + hasClearButton: Bool = false, + isSecureTextEntry: Bool = false, + returnKeyType: UIReturnKeyType = .done, + keyboardType: UIKeyboardType = .default, + autocapitalizationType: UITextAutocapitalizationType = .sentences, + autocorrectionType: UITextAutocorrectionType = .default, + isInitiallyFocused: Bool = false, + externalState: ExternalState, + shouldChangeText: ((String) -> Bool)? = nil, + returnKeyAction: (() -> Void)? = nil + ) { + self.context = context + self.initialValue = initialValue + self.placeholder = placeholder + self.characterLimit = characterLimit + self.hasClearButton = hasClearButton + self.isSecureTextEntry = isSecureTextEntry + self.returnKeyType = returnKeyType + self.keyboardType = keyboardType + self.autocapitalizationType = autocapitalizationType + self.autocorrectionType = autocorrectionType + self.isInitiallyFocused = isInitiallyFocused + self.externalState = externalState + self.shouldChangeText = shouldChangeText + self.returnKeyAction = returnKeyAction + } + + public static func ==(lhs: AlertInputFieldComponent, rhs: AlertInputFieldComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.initialValue != rhs.initialValue { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.characterLimit != rhs.characterLimit { + return false + } + if lhs.hasClearButton != rhs.hasClearButton { + return false + } + if lhs.isSecureTextEntry != rhs.isSecureTextEntry { + return false + } + if lhs.returnKeyType != rhs.returnKeyType { + return false + } + if lhs.keyboardType != rhs.keyboardType { + return false + } + if lhs.autocapitalizationType != rhs.autocapitalizationType { + return false + } + if lhs.autocorrectionType != rhs.autocorrectionType { + return false + } + if lhs.isInitiallyFocused != rhs.isInitiallyFocused { + return false + } + return true + } + + private final class TextField: UITextField { + var sideInset: CGFloat = 0.0 + + override func textRect(forBounds bounds: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) + } + } + + public final class View: UIView, UITextFieldDelegate { + private let background = ComponentView() + private let textField = TextField() + private let placeholder = ComponentView() + private let clearButton = ComponentView() + + private var component: AlertInputFieldComponent? + private weak var state: EmptyComponentState? + + private var isUpdating = false + + var currentText: String { + return self.textField.text ?? "" + } + + func activateInput() { + self.textField.becomeFirstResponder() + } + + func animateError() { + self.textField.layer.addShakeAnimation() + + HapticFeedback().error() + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.component?.returnKeyAction?() + return false + } + + @objc private func textDidChange() { + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + self.clearButton.view?.isHidden = self.currentText.isEmpty + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + self.clearButton.view?.isHidden = true + } + + public func setText(text: String) { + self.textField.text = text + if !self.isUpdating { + self.state?.updated(transition: .immediate, isLocal: true) + } + } + + func update(component: AlertInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + var resetText: String? + if self.component == nil { + resetText = component.initialValue + component.externalState.animateError = { [weak self] in + self?.animateError() + } + component.externalState.activateInput = { [weak self] in + self?.activateInput() + } + } + + let isFirstTime = self.component == nil + + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let topInset: CGFloat = 15.0 + + if self.textField.superview == nil { + self.addSubview(self.textField) + self.textField.delegate = self + self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged) + } + if self.textField.autocapitalizationType != component.autocapitalizationType { + self.textField.autocapitalizationType = component.autocapitalizationType + } + if self.textField.autocorrectionType != component.autocorrectionType { + self.textField.autocorrectionType = component.autocorrectionType + } + if self.textField.isSecureTextEntry != component.isSecureTextEntry { + self.textField.isSecureTextEntry = component.isSecureTextEntry + } + if self.textField.returnKeyType != component.returnKeyType { + self.textField.returnKeyType = component.returnKeyType + } + self.textField.keyboardAppearance = environment.theme.overallDarkAppearance ? .dark : .light + if let resetText { + self.textField.text = resetText + } + + self.textField.font = Font.regular(17.0) + self.textField.textColor = environment.theme.actionSheet.primaryTextColor + self.textField.tintColor = environment.theme.actionSheet.controlAccentColor + self.textField.sideInset = 16.0 + + let backgroundPadding: CGFloat = 14.0 + let size = CGSize(width: availableSize.width, height: 50.0) + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent( + FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false) + ), + environment: {}, + containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height) + ) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let textFieldSize = CGSize(width: availableSize.width - 24.0, height: 50.0) + let textFieldFrame = CGRect(origin: CGPoint(x: -12.0, y: topInset), size: textFieldSize) + transition.setFrame(view: self.textField, frame: textFieldFrame) + + let placeholderSize = self.placeholder.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString( + string: component.placeholder, + font: Font.regular(17.0), + textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4) + ))) + ), + environment: {}, + containerSize: CGSize(width: size.width, height: 50.0) + ) + let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize) + if let placeholderView = self.placeholder.view { + if placeholderView.superview == nil { + placeholderView.isUserInteractionEnabled = false + self.addSubview(placeholderView) + } + placeholderView.frame = placeholderFrame + placeholderView.isHidden = !self.currentText.isEmpty + } + + if component.hasClearButton { + let clearButtonSize = self.clearButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: "Components/Search Bar/Clear", + tintColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4) + )), + effectAlignment: .center, + minSize: CGSize(width: 44.0, height: 44.0), + action: { [weak self] in + guard let self else { + return + } + self.setText(text: "") + }, + animateAlpha: false, + animateScale: true + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + if let clearButtonView = self.clearButton.view { + if clearButtonView.superview == nil { + self.addSubview(clearButtonView) + } + transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - clearButtonSize.width + 11.0, y: topInset + floor((size.height - clearButtonSize.height) * 0.5)), size: clearButtonSize)) + clearButtonView.isHidden = self.currentText.isEmpty || !self.textField.isFirstResponder + } + } else if let clearButtonView = self.clearButton.view, clearButtonView.superview != nil { + clearButtonView.removeFromSuperview() + } + + if isFirstTime && component.isInitiallyFocused { + self.activateInput() + } + + component.externalState.value = self.currentText + component.externalState.valuePromise.set(self.currentText) + + return CGSize(width: availableSize.width, height: size.height + topInset) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift index 93e6b133d9..49f9b1e7d7 100644 --- a/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift @@ -18,6 +18,7 @@ public final class AlertMultilineInputFieldComponent: Component { public class ExternalState { public fileprivate(set) var value: NSAttributedString = NSAttributedString() public fileprivate(set) var animateError: () -> Void = {} + public fileprivate(set) var activateInput: () -> Void = {} fileprivate let valuePromise = ValuePromise(NSAttributedString()) public var valueSignal: Signal { return self.valuePromise.get() @@ -115,6 +116,7 @@ public final class AlertMultilineInputFieldComponent: Component { let placeholder: String let prefix: NSAttributedString? let characterLimit: Int? + let returnKeyType: UIReturnKeyType let keyboardType: UIKeyboardType let autocapitalizationType: UITextAutocapitalizationType let autocorrectionType: UITextAutocorrectionType @@ -131,6 +133,7 @@ public final class AlertMultilineInputFieldComponent: Component { placeholder: String, prefix: NSAttributedString? = nil, characterLimit: Int? = nil, + returnKeyType: UIReturnKeyType = .default, keyboardType: UIKeyboardType = .default, autocapitalizationType: UITextAutocapitalizationType = .sentences, autocorrectionType: UITextAutocorrectionType = .default, @@ -146,6 +149,7 @@ public final class AlertMultilineInputFieldComponent: Component { self.placeholder = placeholder self.prefix = prefix self.characterLimit = characterLimit + self.returnKeyType = returnKeyType self.keyboardType = keyboardType self.autocapitalizationType = autocapitalizationType self.autocorrectionType = autocorrectionType @@ -170,6 +174,9 @@ public final class AlertMultilineInputFieldComponent: Component { if lhs.prefix != rhs.prefix { return false } + if lhs.returnKeyType != rhs.returnKeyType { + return false + } if lhs.characterLimit != rhs.characterLimit { return false } @@ -203,6 +210,12 @@ public final class AlertMultilineInputFieldComponent: Component { private var component: AlertMultilineInputFieldComponent? private weak var state: EmptyComponentState? + func activateInput() { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.activateInput() + } + } + func animateError() { if let textFieldView = self.textField.view { textFieldView.layer.addShakeAnimation() @@ -217,6 +230,9 @@ public final class AlertMultilineInputFieldComponent: Component { component.externalState.animateError = { [weak self] in self?.animateError() } + component.externalState.activateInput = { [weak self] in + self?.activateInput() + } } let isFirstTime = self.component == nil @@ -228,6 +244,7 @@ public final class AlertMultilineInputFieldComponent: Component { let topInset: CGFloat = 15.0 let horizontalInset: CGFloat = 4.0 + let verticalInset: CGFloat = 11.0 - UIScreenPixel let textFieldSize = self.textField.update( transition: transition, @@ -247,7 +264,7 @@ public final class AlertMultilineInputFieldComponent: Component { characterLimit: component.characterLimit, emptyLineHandling: component.emptyLineHandling.textFieldValue, formatMenuAvailability: component.formatMenuAvailability.textFieldValue, - returnKeyType: .done, + returnKeyType: component.returnKeyType, keyboardType: component.keyboardType, autocapitalizationType: component.autocapitalizationType, autocorrectionType: component.autocorrectionType, @@ -277,7 +294,7 @@ public final class AlertMultilineInputFieldComponent: Component { component.externalState.valuePromise.set(component.externalState.value) let backgroundPadding: CGFloat = 14.0 - let size = CGSize(width: availableSize.width, height: max(50.0, textFieldSize.height + 22.0)) + let size = CGSize(width: availableSize.width, height: max(50.0, floor(textFieldSize.height + verticalInset * 2.0))) let backgroundSize = self.background.update( transition: transition, @@ -327,9 +344,7 @@ public final class AlertMultilineInputFieldComponent: Component { } if isFirstTime && component.isInitiallyFocused { - if let textFieldView = self.textField.view as? TextFieldComponent.View { - textFieldView.activateInput() - } + self.activateInput() } return CGSize(width: availableSize.width, height: size.height + topInset) diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift index 4f330f6bcc..30cbac294a 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift @@ -161,17 +161,14 @@ final class AlertActionComponent: Component { activity = ComponentView() self.activity = activity } - let activitySize = activity.update( + let activitySize = CGSize(width: 18.0, height: 18.0) + let _ = activity.update( transition: transition, component: AnyComponent(ActivityIndicatorComponent(color: component.theme.secondary)), environment: {}, - containerSize: availableSize + containerSize: activitySize ) 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 { @@ -207,9 +204,16 @@ final class AlertActionComponent: Component { } if let activityView = self.activity?.view { + var activityTransition = transition + if activityView.superview == nil { + self.addSubview(activityView) + transition.animateAlpha(view: activityView, from: 0.0, to: 1.0) + activityTransition = .immediate + } 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) + activityTransition.setPosition(view: activityView, position: activityFrame.center) + activityView.transform = CGAffineTransformMakeScale(0.7, 0.7) } } } diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift index 8c82ff746e..efc2346143 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -373,7 +373,7 @@ private final class AlertScreenComponent: Component { let defaultActionTheme = AlertActionComponent.Theme( background: environment.theme.actionSheet.controlAccentColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, - secondary: environment.theme.actionSheet.secondaryTextColor, + secondary: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.85), font: .bold ) let destructiveActionTheme = AlertActionComponent.Theme( diff --git a/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD b/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD new file mode 100644 index 0000000000..014f566a64 --- /dev/null +++ b/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "CocoonInfoScreen", + module_name = "CocoonInfoScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/PresentationDataUtils", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/SheetComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift b/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift new file mode 100644 index 0000000000..61d04d1edf --- /dev/null +++ b/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift @@ -0,0 +1,669 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BalancedTextComponent +import BundleIconComponent +import Markdown +import TextFormat +import TelegramStringFormatting +import GlassBarButtonComponent +import ButtonComponent +import LottieComponent + +private final class CocoonInfoSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let animateOut: ActionSlot> + let getController: () -> ViewController? + + init( + context: AccountContext, + animateOut: ActionSlot>, + getController: @escaping () -> ViewController? + ) { + self.context = context + self.animateOut = animateOut + self.getController = getController + } + + static func ==(lhs: CocoonInfoSheetContent, rhs: CocoonInfoSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private let animateOut: ActionSlot> + private let getController: () -> ViewController? + + fileprivate let playButtonAnimation = ActionSlot() + private var didPlayAnimation = false + + init( + context: AccountContext, + animateOut: ActionSlot>, + getController: @escaping () -> ViewController? + ) { + self.context = context + self.animateOut = animateOut + self.getController = getController + + super.init() + } + + func playAnimationIfNeeded() { + if !self.didPlayAnimation { + self.didPlayAnimation = true + self.playButtonAnimation.invoke(Void()) + } + } + + func dismiss(animated: Bool) { + guard let controller = self.getController() as? CocoonInfoScreen else { + return + } + if animated { + self.animateOut.invoke(Action { [weak controller] _ in + controller?.dismiss(completion: nil) + }) + } else { + controller.dismiss(animated: false) + } + } + } + + func makeState() -> State { + return State(context: self.context, animateOut: self.animateOut, getController: self.getController) + } + + static var body: Body { + let closeButton = Child(GlassBarButtonComponent.self) + let icon = Child(BundleIconComponent.self) + let title = Child(BalancedTextComponent.self) + let text = Child(BalancedTextComponent.self) + let list = Child(List.self) + let additionalText = Child(MultilineTextComponent.self) + let button = Child(ButtonComponent.self) + + let navigateDisposable = MetaDisposable() + + return { context in + let component = context.component + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let state = context.state + + let theme = environment.theme + let strings = environment.strings + + let sideInset: CGFloat = 30.0 + environment.safeInsets.left + let textSideInset: CGFloat = 30.0 + environment.safeInsets.left + + let titleFont = Font.bold(24.0) + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + + let textColor = theme.actionSheet.primaryTextColor + let secondaryTextColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + + let spacing: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 28.0) + + let icon = icon.update( + component: BundleIconComponent( + name: "Premium/Cocoon", tintColor: nil + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(icon + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + icon.size.height / 2.0)) + ) + contentSize.height += icon.size.height + contentSize.height += 14.0 + + let title = title.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: strings.CocoonInfo_Title, font: titleFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += spacing - 8.0 + + let attributedText = parseMarkdownIntoAttributedString( + strings.CocoonInfo_Description, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: linkColor), + linkAttribute: { _ in return nil } + ) + ) + let text = text.update( + component: BalancedTextComponent( + text: .plain(attributedText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, 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 += spacing + 9.0 + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "private", + component: AnyComponent(ParagraphComponent( + title: strings.CocoonInfo_Private_Title, + titleColor: textColor, + text: strings.CocoonInfo_Private_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Ads/Privacy", + iconColor: linkColor, + action: { _, _ in + } + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "efficient", + component: AnyComponent(ParagraphComponent( + title: strings.CocoonInfo_Efficient_Title, + titleColor: textColor, + text: strings.CocoonInfo_Efficient_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/Stats", + iconColor: linkColor, + action: { _, _ in + } + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "for_everyone", + component: AnyComponent(ParagraphComponent( + title: strings.CocoonInfo_ForEveryone_Title, + titleColor: textColor, + text: strings.CocoonInfo_ForEveryone_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Chat/Input/Accessory Panels/Gift", + iconColor: linkColor, + action: { attributes, _ in + guard let link = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, let controller = environment.controller() else { + return + } + switch link { + case "telegram": + component.context.sharedContext.handleTextLinkAction(context: component.context, peerId: nil, navigateDisposable: navigateDisposable, controller: controller, action: .tap, itemLink: .url(url: "https://t.me/cocoon", concealed: false)) + case "web": + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: "https://cocoon.org", forceExternal: true, presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + default: + break + } + } + )) + ) + ) + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 10000.0), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) + ) + contentSize.height += list.size.height + contentSize.height += spacing - 6.0 + + let attributedAdditionalText = parseMarkdownIntoAttributedString( + strings.CocoonInfo_IntergrateInfo, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: secondaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: secondaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + ) + let additionalText = additionalText.update( + component: MultilineTextComponent( + text: .plain(attributedAdditionalText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + guard let controller = environment.controller() else { + return + } + component.context.sharedContext.handleTextLinkAction(context: component.context, peerId: nil, navigateDisposable: navigateDisposable, controller: controller, action: .tap, itemLink: .url(url: "https://t.me/cocoon?direct", concealed: false)) + } + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(additionalText + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + additionalText.size.height / 2.0)) + ) + contentSize.height += additionalText.size.height + contentSize.height += spacing + 6.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.chat.inputPanel.panelControlColor + ) + )), + action: { [weak state] _ in + guard let state else { + return + } + state.dismiss(animated: true) + } + ), + 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)) + ) + + + var buttonTitle: [AnyComponentWithIdentity] = [] + buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_ok"), + color: theme.list.itemCheckColors.foregroundColor, + startingPosition: .begin, + size: CGSize(width: 28.0, height: 28.0), + playOnce: state.playButtonAnimation + )))) + buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent( + text: strings.CocoonInfo_Understood, + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )))) + + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonTitle, spacing: 2.0)) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak state] in + guard let state else { + return + } + state.dismiss(animated: true) + } + ), + availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, 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 += 30.0 + + state.playAnimationIfNeeded() + + return contentSize + } + } +} + +final class CocoonInfoSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: CocoonInfoSheetComponent, rhs: CocoonInfoSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(CocoonInfoSheetContent( + context: context.component.context, + animateOut: animateOut, + getController: controller + )), + style: .glass, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + autoAnimateOut: false, + externalState: sheetExternalState, + animateOut: animateOut, + onPan: { + }, + willDismiss: { + } + ), + 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 { + if let controller = controller() as? CocoonInfoScreen { + animateOut.invoke(Action { _ in + controller.dismiss(completion: nil) + }) + } + } else { + if let controller = controller() as? CocoonInfoScreen { + 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)) + ) + + 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 + } + } +} + +public final class CocoonInfoScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext + ) { + self.context = context + + super.init( + context: context, + component: CocoonInfoSheetComponent( + context: context + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class ParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let accentColor: UIColor + let iconName: String + let iconColor: UIColor + let action: (([NSAttributedString.Key: Any], Int) -> Void)? + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + accentColor: UIColor, + iconName: String, + iconColor: UIColor, + action: (([NSAttributedString.Key: Any], Int) -> Void)? + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.accentColor = accentColor + self.iconName = iconName + self.iconColor = iconColor + self.action = action + } + + static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + + let leftInset: CGFloat = 32.0 + let rightInset: CGFloat = 24.0 + let textSideInset: CGFloat = leftInset + 8.0 + let spacing: CGFloat = 5.0 + + let textTopInset: CGFloat = 9.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.semibold(15.0), + textColor: component.titleColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = component.textColor + let accentColor = component.accentColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: accentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: accentColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, index in + component.action?(attributes, index) + } + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.iconColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: 15.0, y: textTopInset + 18.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0) + } + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index 13132bc01b..ae2cebc6c4 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -514,8 +514,8 @@ final class EntityKeyboardBottomPanelComponent: Component { lensSelection = (selectionGestureState.currentX, lensSelection.width) } - transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabsSize.width) / 2.0), y: 0.0), size: tabsSize)) - self.liquidLensView.update(size: tabsSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: tabsSize.height), inset: 0.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: activeContentId == nil, transition: transition) + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabsSize.width) / 2.0), y: 3.0), size: tabsSize)) + self.liquidLensView.update(size: CGSize(width: tabsSize.width, height: tabsSize.height), selectionOrigin: CGPoint(x: max(0.0, min(tabsSize.width - lensSelection.width, lensSelection.x)), y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: tabsSize.height), inset: 3.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: activeContentId == nil, transition: transition) var removedIconViewIds: [AnyHashable] = [] for (id, iconView) in self.itemViews { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift index d9cf921a9a..5c7abf8318 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift @@ -8,347 +8,287 @@ import TelegramPresentationData import AccountContext import UrlEscaping import ComponentFlow +import AlertComponent import StarsWithdrawalScreen -private final class GiftAuctionCustomBidAlertContentNode: AlertContentNode { - private let theme: PresentationTheme - private let strings: PresentationStrings - private let dateTimeFormat: PresentationDateTimeFormat - private let title: String - private let text: String - private let placeholder: String - private let minValue: Int64 - fileprivate var value: Int64 +func giftAuctionCustomBidController( + context: AccountContext, + title: String, + text: String, + placeholder: String, + action: String, + minValue: Int64, + value: Int64, + apply: @escaping (Int64) -> Void, + cancel: @escaping () -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings - private let titleNode: ASTextNode - private let textNode: ASTextNode - private let backgroundView = UIImageView() - let amountField = ComponentView() - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - //self.inputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, actions: [TextAlertAction], title: String, text: String, placeholder: String, minValue: Int64, value: Int64) { - self.theme = ptheme - self.strings = strings - self.dateTimeFormat = dateTimeFormat - self.title = title - self.text = text - self.placeholder = placeholder - self.minValue = minValue - self.value = value + let inputState = AlertAmountFieldComponent.ExternalState() - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 8 - -// self.inputFieldNode = GiftAuctionCustomBidInputFieldNode(theme: ptheme, placeholder: placeholder) -// self.inputFieldNode.text = "\(value)" - - self.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.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - 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) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 5.0 - - let titleSize = self.titleNode.measure(measureSize) - 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(measureSize) - 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 + 6.0 + spacing - - 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: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let fieldWidth = resultWidth - 18.0 - let amountFieldSize = self.amountField.update( - transition: .immediate, - component: AnyComponent( - AmountFieldComponent( - textColor: self.theme.list.itemPrimaryTextColor, - secondaryColor: self.theme.list.itemSecondaryTextColor, - placeholderColor: self.theme.list.itemPlaceholderTextColor, - accentColor: self.theme.list.itemAccentColor, - value: self.value, - minValue: self.minValue, - forceMinValue: false, - allowZero: false, - maxValue: nil, - placeholderText: self.placeholder, - textFieldOffset: CGPoint(x: -4.0, y: -1.0), - labelText: nil, - currency: .stars, - dateTimeFormat: self.dateTimeFormat, - amountUpdated: { [weak self] value in - guard let self else { - return - } - if let value { - self.value = value - } - }, - tag: nil - ) - ), - environment: {}, - containerSize: CGSize(width: fieldWidth, height: 44.0) + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) ) - var amountFieldFrame = CGRect(origin: CGPoint(x: floor((resultWidth - fieldWidth) / 2.0), y: origin.y - 2.0), size: amountFieldSize) - if let amountFieldView = self.amountField.view { - if amountFieldView.superview == nil { - amountFieldView.clipsToBounds = true - self.backgroundView.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.view.addSubview(self.backgroundView) - self.view.addSubview(amountFieldView) - } - self.backgroundView.frame = amountFieldFrame.insetBy(dx: 7.0, dy: 9.0) - amountFieldFrame.size.width -= 14.0 - amountFieldView.frame = amountFieldFrame - } - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + amountFieldSize.height + actionsHeight + insets.top + insets.bottom + 3.0) - - transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - - 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))) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + + var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertAmountFieldComponent( + context: context, + initialValue: value, + minValue: minValue, + maxValue: nil, + placeholder: placeholder, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() } - } - 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 + ) + ) + )) + + let alertController = AlertScreen( + context: context, + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel, action: { + cancel() + }), + .init(title: action, type: .default, action: { + applyImpl?() + }, autoDismiss: false) + ] + ) + applyImpl = { + if let value = inputState.value, value >= minValue { + apply(value) + } else { + inputState.resetToMinValue() + inputState.animateError() + } + } + return alertController +} + +private final class AlertAmountFieldComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public class ExternalState { + public fileprivate(set) var value: Int64? + public fileprivate(set) var animateError: () -> Void = {} + public fileprivate(set) var activateInput: () -> Void = {} + public fileprivate(set) var resetToMinValue: () -> Void = {} + fileprivate let valuePromise = ValuePromise(nil) + public var valueSignal: Signal { + return self.valuePromise.get() } - if !hadValidLayout { + public init() { + } + } + + let context: AccountContext + let initialValue: Int64? + let minValue: Int64? + let maxValue: Int64? + let placeholder: String + let isInitiallyFocused: Bool + let externalState: ExternalState + let returnKeyAction: (() -> Void)? + + public init( + context: AccountContext, + initialValue: Int64? = nil, + minValue: Int64? = nil, + maxValue: Int64? = nil, + placeholder: String, + isInitiallyFocused: Bool = false, + externalState: ExternalState, + returnKeyAction: (() -> Void)? = nil + ) { + self.context = context + self.initialValue = initialValue + self.minValue = minValue + self.maxValue = maxValue + self.placeholder = placeholder + self.isInitiallyFocused = isInitiallyFocused + self.externalState = externalState + self.returnKeyAction = returnKeyAction + } + + public static func ==(lhs: AlertAmountFieldComponent, rhs: AlertAmountFieldComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.initialValue != rhs.initialValue { + return false + } + if lhs.minValue != rhs.minValue { + return false + } + if lhs.maxValue != rhs.maxValue { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.isInitiallyFocused != rhs.isInitiallyFocused { + return false + } + return true + } + + public final class View: UIView, UITextFieldDelegate { + private let background = ComponentView() + private let amountField = ComponentView() + + private var currentValue: Int64? + + private var component: AlertAmountFieldComponent? + private weak var state: EmptyComponentState? + + private var isUpdating = false + + func activateInput() { if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { amountFieldView.activateInput() - Queue.mainQueue().justDispatch { - amountFieldView.selectAll() - } } } - return resultSize - } - - func animateError() { - if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { - self.value = self.minValue - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - amountFieldView.resetValue() + func resetToMinValue() { + self.currentValue = self.component?.minValue + self.state?.updated() - amountFieldView.animateError() - amountFieldView.selectAll() + if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { + amountFieldView.resetValue() + amountFieldView.selectAll() + } + } + + func animateError() { + if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { + amountFieldView.animateError() + } + } + + func update(component: AlertAmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.currentValue = component.initialValue + + component.externalState.animateError = { [weak self] in + self?.animateError() + } + component.externalState.activateInput = { [weak self] in + self?.activateInput() + } + component.externalState.resetToMinValue = { [weak self] in + self?.resetToMinValue() + } + } + + let isFirstTime = self.component == nil + + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let topInset: CGFloat = 15.0 + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let amountFieldSize = self.amountField.update( + transition: .immediate, + component: AnyComponent( + AmountFieldComponent( + textColor: environment.theme.actionSheet.primaryTextColor, + secondaryColor: environment.theme.actionSheet.secondaryTextColor, + placeholderColor: environment.theme.actionSheet.inputPlaceholderColor, + accentColor: environment.theme.actionSheet.controlAccentColor, + value: self.currentValue, + minValue: component.minValue, + forceMinValue: false, + allowZero: false, + maxValue: nil, + placeholderText: component.placeholder, + textFieldOffset: CGPoint(x: -4.0, y: -1.0), + labelText: nil, + currency: .stars, + dateTimeFormat: presentationData.dateTimeFormat, + amountUpdated: { [weak self] value in + guard let self else { + return + } + self.currentValue = value + component.externalState.value = value + component.externalState.valuePromise.set(value) + }, + tag: nil + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + var amountFieldFrame = CGRect(origin: CGPoint(x: -16.0, y: topInset - 1.0 + UIScreenPixel), size: amountFieldSize) + if let amountFieldView = self.amountField.view { + if amountFieldView.superview == nil { + amountFieldView.clipsToBounds = true + self.addSubview(amountFieldView) + } + amountFieldFrame.size.width -= 14.0 + amountFieldView.frame = amountFieldFrame + } + + let backgroundPadding: CGFloat = 14.0 + let size = CGSize(width: availableSize.width, height: 50.0) + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent( + FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false) + ), + environment: {}, + containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height) + ) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + if isFirstTime && component.isInitiallyFocused { + self.activateInput() + } + + return CGSize(width: availableSize.width, height: size.height + topInset) } } - func deactivateInput() { - if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { - amountFieldView.deactivateInput() - } + 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) } } - -func giftAuctionCustomBidController(context: AccountContext, title: String, text: String, placeholder: String, action: String, minValue: Int64, value: Int64, apply: @escaping (Int64) -> Void, cancel: @escaping () -> Void) -> AlertController { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - var dismissImpl: ((Bool) -> Void)? - var applyImpl: (() -> Void)? - - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - cancel() - }), TextAlertAction(type: .defaultAction, title: action, action: { - applyImpl?() - })] - - let contentNode = GiftAuctionCustomBidAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, actions: actions, title: title, text: text, placeholder: placeholder, minValue: minValue, value: value) - contentNode.complete = { - applyImpl?() - } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - let value = contentNode.value - if value < minValue { - contentNode.animateError() - } else { - dismissImpl?(true) - apply(contentNode.value) - } - } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - }) - controller.dismissed = { byTapOutside in - presentationDataDisposable.dispose() - if byTapOutside { - cancel() - } - } - dismissImpl = { [weak controller, weak contentNode] animated in - contentNode?.deactivateInput() - let _ = contentNode - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller -} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index a18bc10a2d..774ce219f0 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -3011,9 +3011,7 @@ private final class GiftViewSheetContent: CombinedComponent { originY += 16.0 } else if showUpgradePreview { let title: String - //let description: String let uniqueText: String - //let transferableText: String let tradableText: String if !incoming, case let .profileGift(peerId, _) = subject, let peer = state.peerMap[peerId] { var peerName = peer.compactDisplayTitle @@ -3021,9 +3019,7 @@ private final class GiftViewSheetContent: CombinedComponent { peerName = "\(peerName.prefix(22))…" } title = environment.strings.Gift_Upgrade_GiftTitle - // description = environment.strings.Gift_Upgrade_GiftDescription(peerName).string uniqueText = strings.Gift_Upgrade_Unique_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 @@ -3031,15 +3027,11 @@ private final class GiftViewSheetContent: CombinedComponent { peerName = "\(peerName.prefix(22))…" } title = environment.strings.Gift_Upgrade_IncludeTitle - // description = environment.strings.Gift_Upgrade_IncludeDescription(peerName).string uniqueText = strings.Gift_Upgrade_Unique_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 uniqueText = strings.Gift_Upgrade_Unique_Description - // transferableText = strings.Gift_Upgrade_Transferable_Description tradableText = strings.Gift_Upgrade_Tradable_Description } @@ -3126,7 +3118,7 @@ private final class GiftViewSheetContent: CombinedComponent { ) )), AnyComponentWithIdentity(id: "text", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "View all variants", font: Font.semibold(13.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_ViewAllVariants, font: Font.semibold(13.0), textColor: .white))) )), AnyComponentWithIdentity(id: "arrow", component: AnyComponent( BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: .white) @@ -3181,20 +3173,6 @@ 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: "tradable", @@ -3209,17 +3187,16 @@ private final class GiftViewSheetContent: CombinedComponent { )) ) ) - //TODO:localize items.append( AnyComponentWithIdentity( id: "wearable", component: AnyComponent(InfoParagraphComponent( - title: "Wearable", + title: strings.Gift_Upgrade_Wearable_Title, titleColor: textColor, - text: "Display gifts on your page and set them as profile covers or statuses.", + text: strings.Gift_Upgrade_Wearable_Text, textColor: secondaryTextColor, accentColor: linkColor, - iconName: "Premium/Collectible/Transferable", + iconName: "Premium/Collectible/Wearable", iconColor: linkColor )) ) diff --git a/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift b/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift index 176bccca06..d1449ab095 100644 --- a/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift +++ b/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift @@ -504,4 +504,21 @@ public final class GlobalControlPanelsContext { impl.dismissChatListNotice(parentController: parentController, notice: notice) } } + + public var tempVoicePlaylistEnded: (() -> Void)? { + didSet { + let tempVoicePlaylistEnded = self.tempVoicePlaylistEnded + self.impl.with { impl in + impl.tempVoicePlaylistEnded = tempVoicePlaylistEnded + } + } + } + public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)? { + didSet { + let tempVoicePlaylistItemChanged = self.tempVoicePlaylistItemChanged + self.impl.with { impl in + impl.tempVoicePlaylistItemChanged = tempVoicePlaylistItemChanged + } + } + } } diff --git a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift index 480ec2ea73..8c974a43ef 100644 --- a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift @@ -49,7 +49,6 @@ private final class SheetContent: CombinedComponent { final class State: ComponentState { var cachedChevronImage: (UIImage, PresentationTheme)? - var cachedCloseImage: (UIImage, PresentationTheme)? let playOnce = ActionSlot() private var didPlayAnimation = false @@ -189,14 +188,15 @@ private final class SheetContent: CombinedComponent { contentSize.height += list.size.height contentSize.height += spacing + 2.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonAttributedString = NSMutableAttributedString(string: strings.FrozenAccount_SubmitAppeal, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let actionButton = actionButton.update( component: 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.9), - cornerRadius: 10.0 ), content: AnyComponentWithIdentity( id: AnyHashable(0), @@ -209,7 +209,7 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: context.transition ) context.add(actionButton @@ -223,10 +223,10 @@ private final class SheetContent: CombinedComponent { let closeButton = closeButton.update( component: ButtonComponent( background: ButtonComponent.Background( - color: .clear, - foreground: .clear, - pressedColor: .clear, - cornerRadius: 10.0 + style: .glass, + color: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.1), + foreground: theme.list.itemCheckColors.fillColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity( id: AnyHashable(1), @@ -238,19 +238,14 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: context.transition ) context.add(closeButton .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) ) contentSize.height += closeButton.size.height - - if environment.safeInsets.bottom > 0 { - contentSize.height += environment.safeInsets.bottom + 5.0 - } else { - contentSize.height += 12.0 - } + contentSize.height += buttonInsets.bottom state.playAnimationIfNeeded() @@ -312,6 +307,7 @@ private final class SheetContainerComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, externalState: sheetExternalState, @@ -507,8 +503,8 @@ private final class ParagraphComponent: CombinedComponent { textColor: component.titleColor, paragraphAlignment: .natural )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 + horizontalAlignment: .natural, + maximumNumberOfLines: 2 ), availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), transition: .immediate @@ -576,24 +572,3 @@ private final class ParagraphComponent: CombinedComponent { } } } - -private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(foregroundColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} diff --git a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD index 174ee14b59..dd67a3b410 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD +++ b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD @@ -20,6 +20,8 @@ swift_library( "//submodules/ComponentFlow", "//submodules/Components/MultilineTextComponent", "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift index b5bd40fb9d..8a531ac3a3 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift @@ -10,533 +10,73 @@ import AccountContext import ComponentFlow import MultilineTextComponent import BalancedTextComponent +import AlertComponent +import AlertInputFieldComponent -private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - private let textInputNode: EditableTextNode - private let placeholderNode: ASTextNode - private let characterLimitView = ComponentView() - - private let characterLimit: Int - - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? - - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets: UIEdgeInsets - - private let validCharacterSets: [CharacterSet] - - var text: String { - get { - return self.textInputNode.attributedText?.string ?? "" - } - set { - self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputTextColor) - self.placeholderNode.isHidden = !newValue.isEmpty - } - } - - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - init(theme: PresentationTheme, placeholder: String, characterLimit: Int) { - self.theme = theme - self.characterLimit = characterLimit - - self.inputInsets = UIEdgeInsets(top: 9.0, left: 6.0, bottom: 9.0, right: 16.0) - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = EditableTextNode() - self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(13.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] - self.textInputNode.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: self.inputInsets.left, bottom: self.inputInsets.bottom, right: self.inputInsets.right) - self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.keyboardType = .default - self.textInputNode.autocapitalizationType = .none - self.textInputNode.returnKeyType = .done - self.textInputNode.autocorrectionType = .no - self.textInputNode.tintColor = theme.actionSheet.controlAccentColor - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - self.validCharacterSets = [ - CharacterSet.alphanumerics, - CharacterSet(charactersIn: "0123456789_ "), - ] - - super.init() - - self.textInputNode.delegate = self - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) - - let characterLimitString: String - let characterLimitColor: UIColor - if self.text.count <= self.characterLimit { - let remaining = self.characterLimit - self.text.count - if remaining < 5 { - characterLimitString = "\(remaining)" - } else { - characterLimitString = " " - } - characterLimitColor = self.theme.list.itemPlaceholderTextColor - } else { - characterLimitString = "\(self.characterLimit - self.text.count)" - characterLimitColor = self.theme.list.itemDestructiveColor - } - - let characterLimitSize = self.characterLimitView.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: characterLimitString, font: Font.regular(13.0), textColor: characterLimitColor)) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) +public func businessLinkNameAlertController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + value: String?, + apply: @escaping (String?) -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let inputState = AlertInputFieldComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Business_Links_LinkNameTitle) ) - if let characterLimitComponentView = self.characterLimitView.view { - if characterLimitComponentView.superview == nil { - self.view.addSubview(characterLimitComponentView) - } - characterLimitComponentView.frame = CGRect(origin: CGPoint(x: width - 23.0 - characterLimitSize.width, y: 18.0), size: characterLimitSize) - } - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.updateTextNodeText(animated: true) - self.textChanged?(editableTextNode.textView.text) - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty - } - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if text == "\n" { - self.complete?() - return false - } - if text.unicodeScalars.contains(where: { c in - return !self.validCharacterSets.contains(where: { set in - return set.contains(c) - }) - }) { - return false - } - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(34.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(34.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.textInputNode.attributedText = nil - self.deactivateInput() - } -} - -public final class BusinessLinkNameAlertContentNode: AlertContentNode { - private let context: AccountContext - private var theme: AlertControllerTheme - private let strings: PresentationStrings - private let text: String - private let subtext: String - private let titleFont: PromptControllerTitleFont - - private let textView = ComponentView() - private let subtextView = ComponentView() - - fileprivate let inputFieldNode: PromptInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - private var errorText: String? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? - - override public var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, subtext: String, titleFont: PromptControllerTitleFont, value: String?, characterLimit: Int) { - self.context = context - self.theme = theme - self.strings = strings - self.text = text - self.subtext = subtext - self.titleFont = titleFont - - self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: strings.Business_Links_LinkNameInputPlaceholder, characterLimit: characterLimit) - self.inputFieldNode.text = value ?? "" - - 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.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - self.actionNodes.last?.actionEnabled = true - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.immediate) - } - } - } - - self.inputFieldNode.textChanged = { [weak self] text in - if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { - lastNode.actionEnabled = text.count <= characterLimit - strongSelf.requestLayout?(.immediate) - } - } - - self.updateTheme(theme) - - self.inputFieldNode.complete = { [weak self] in - guard let self else { - return - } - if let lastNode = self.actionNodes.last, lastNode.actionEnabled { - self.complete?() - } - } - } - - deinit { - self.disposable.dispose() - } - - var value: String { - return self.inputFieldNode.text - } - - public func setErrorText(errorText: String?) { - if self.errorText != errorText { - self.errorText = errorText - self.requestLayout?(.immediate) - } - - if errorText != nil { - HapticFeedback().error() - self.inputFieldNode.layer.addShakeAnimation() - } - } - - override public func updateTheme(_ theme: AlertControllerTheme) { - self.theme = theme - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 16.0) - let spacing: CGFloat = 5.0 - let subtextSpacing: CGFloat = -1.0 - - let textSize = self.textView.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: measureSize.width, height: 1000.0) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Business_Links_LinkNameText)) ) - let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: origin.y), size: textSize) - if let textComponentView = self.textView.view { - if textComponentView.superview == nil { - textComponentView.layer.anchorPoint = CGPoint() - self.view.addSubview(textComponentView) - } - textComponentView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) - transition.updatePosition(layer: textComponentView.layer, position: textFrame.origin) - } - origin.y += textSize.height + 6.0 + subtextSpacing - - let subtextSize = self.subtextView.update( - transition: .immediate, - component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: self.errorText ?? self.subtext, font: Font.regular(13.0), textColor: self.errorText != nil ? self.theme.destructiveColor : self.theme.primaryColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: measureSize.width, height: 1000.0) - ) - let subtextFrame = CGRect(origin: CGPoint(x: floor((size.width - subtextSize.width) * 0.5), y: origin.y), size: subtextSize) - if let subtextComponentView = self.subtextView.view { - if subtextComponentView.superview == nil { - subtextComponentView.layer.anchorPoint = CGPoint() - self.view.addSubview(subtextComponentView) - } - subtextComponentView.bounds = CGRect(origin: CGPoint(), size: subtextFrame.size) - transition.updatePosition(layer: subtextComponentView.layer, position: subtextFrame.origin) - } - origin.y += subtextSize.height + 6.0 + spacing - - 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: 9.0, right: 18.0) - - var contentWidth = max(textSize.width, minActionsWidth) - contentWidth = max(subtextSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - let inputFieldFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight) - transition.updateFrame(node: self.inputFieldNode, frame: inputFieldFrame) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - let resultSize = CGSize(width: resultWidth, height: textSize.height + subtextSpacing + subtextSize.height + spacing + inputHeight + actionsHeight + 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 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() - } -} + )) -public enum PromptControllerTitleFont { - case regular - case bold -} - -public func businessLinkNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, subtext: String, titleFont: PromptControllerTitleFont = .regular, value: String?, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - - var dismissImpl: ((Bool) -> Void)? var applyImpl: (() -> Void)? - - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - apply(nil) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { - applyImpl?() - })] - - let contentNode = BusinessLinkNameAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, subtext: subtext, titleFont: titleFont, value: value, characterLimit: characterLimit) - contentNode.complete = { - applyImpl?() + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: value, + placeholder: strings.Business_Links_LinkNameInputPlaceholder, + characterLimit: 32, + hasClearButton: false, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - apply(contentNode.value) + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Common_Done, type: .default, action: { + applyImpl?() + }) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + apply(inputState.value) } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller] animated in - contentNode.inputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + return alertController } diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD index a9f96d6606..99a8b1bbc7 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD @@ -21,6 +21,8 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Components/BalancedTextComponent", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift index 9629f3f3bb..7966d579a2 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift @@ -11,6 +11,8 @@ import ComponentFlow import MultilineTextComponent import BalancedTextComponent import EmojiStatusComponent +import AlertComponent +import AlertInputFieldComponent private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { private var theme: PresentationTheme @@ -500,6 +502,100 @@ public enum PromptControllerTitleFont { } public func quickReplyNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, subtext: String, titleFont: PromptControllerTitleFont = .regular, value: String?, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController { +// let presentationData = context.sharedContext.currentPresentationData.with { $0 } +// let strings = presentationData.strings +// +// let inputState = AlertInputFieldComponent.ExternalState() +// +// let doneIsEnabled: Signal = inputState.valueSignal +// |> map { value in +// return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty +// } +// +// let doneInProgressValuePromise = ValuePromise(false) +// let doneInProgress = doneInProgressValuePromise.get() +// +// var content: [AnyComponentWithIdentity] = [] +// content.append(AnyComponentWithIdentity( +// id: "title", +// component: AnyComponent( +// AlertTitleComponent(title: strings.WebBrowser_Exceptions_Create_Title) +// ) +// )) +// content.append(AnyComponentWithIdentity( +// id: "text", +// component: AnyComponent( +// AlertTextComponent(content: .plain(strings.WebBrowser_Exceptions_Create_Text)) +// ) +// )) +// +// let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: []) +// let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: []) +// var applyImpl: (() -> Void)? +// content.append(AnyComponentWithIdentity( +// id: "input", +// component: AnyComponent( +// AlertInputFieldComponent( +// context: context, +// initialValue: nil, +// placeholder: strings.QuickReply_ShortcutPlaceholder, +// characterLimit: characterLimit, +// hasClearButton: false, +// keyboardType: .URL, +// autocapitalizationType: .none, +// autocorrectionType: .no, +// isInitiallyFocused: true, +// externalState: inputState, +// shouldChangeText: { updatedText in +// guard let domainRegex, let pathRegex else { +// return true +// } +// let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) +// let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) +// if domainMatches.count > 0, pathMatches.count == 0 { +// return true +// } else { +// return false +// } +// }, +// returnKeyAction: { +// applyImpl?() +// } +// ) +// ) +// )) +// +// var effectiveUpdatedPresentationData: (PresentationData, Signal) +// if let updatedPresentationData { +// effectiveUpdatedPresentationData = updatedPresentationData +// } else { +// effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) +// } +// +// let alertController = AlertScreen( +// configuration: AlertScreen.Configuration(allowInputInset: true), +// content: content, +// actions: [ +// .init(title: strings.Common_Cancel, action: { +// apply(nil) +// }), +// .init(title: strings.Common_Done, type: .default, action: { +// applyImpl?() +// }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgress) +// ], +// updatedPresentationData: effectiveUpdatedPresentationData +// ) +// applyImpl = { +// let updatedLink = explicitUrl(inputState.value) +// if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) { +// doneInProgressValuePromise.set(true) +// apply(updatedLink) +// } else { +// inputState.animateError() +// } +// } +// return alertController + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } var dismissImpl: ((Bool) -> Void)? diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index eff457d8f6..7677a5af7e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -1646,7 +1646,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { style: .glass, color: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.1), foreground: theme.list.itemCheckColors.fillColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8), + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity( id: AnyHashable(0), diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/Contents.json new file mode 100644 index 0000000000..f3819ec936 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "cocoon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/cocoon.png b/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/cocoon.png new file mode 100644 index 0000000000000000000000000000000000000000..0d5c7455dc88fe8db817518a1687884e68941668 GIT binary patch literal 101385 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

U|=e3H>(BEZ5DFjDtRUg(dmp17HcyNCi@;KKIJ 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 deliverOnMainQueue).startStrict(next: { [weak self] state in diff --git a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift index 85b0428962..4a21f446b8 100644 --- a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift +++ b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift @@ -75,22 +75,19 @@ extension ChatControllerImpl { var completion: ((String?) -> Void)? let alertController = businessLinkNameAlertController( context: self.context, - text: self.presentationData.strings.Business_Links_LinkNameTitle, - subtext: self.presentationData.strings.Business_Links_LinkNameText, value: currentValue, - characterLimit: 32, apply: { value in completion?(value) } ) completion = { [weak self, weak alertController] value in guard let self else { - alertController?.dismissAnimated() + alertController?.dismiss(completion: nil) return } if let value { if value == currentValue { - alertController?.dismissAnimated() + alertController?.dismiss(completion: nil) return } @@ -116,7 +113,7 @@ extension ChatControllerImpl { } alertController?.view.endEditing(true) - alertController?.dismissAnimated() + alertController?.dismiss(completion: nil) } } self.present(alertController, in: .window(.root)) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e6d81edfdc..b6de849147 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -93,6 +93,7 @@ import NewContactScreen import PasskeysScreen import GiftDemoScreen import ChatTextLinkEditUI +import CocoonInfoScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -4024,6 +4025,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return SendInviteLinkScreen(context: context, subject: subject, peers: peers, theme: theme) } + public func makeCocoonInfoScreen(context: AccountContext) -> ViewController { + return CocoonInfoScreen(context: context) + } + public func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController { return chatTextLinkEditController(context: context, updatedPresentationData: updatedPresentationData, text: text, link: link, apply: apply) } From 57cdc60862d457674602e30b21f07d7c0628b160 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 23 Dec 2025 23:54:51 +0400 Subject: [PATCH 4/6] Cocoon info --- Telegram/Telegram-iOS/Resources/Cocoon.tgs | Bin 0 -> 2164 bytes .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/PeerInfoGiftsPaneNode.swift | 2 +- .../Sources/ChatTranslationPanelNode.swift | 146 +++++++++++++++++- 4 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 Telegram/Telegram-iOS/Resources/Cocoon.tgs diff --git a/Telegram/Telegram-iOS/Resources/Cocoon.tgs b/Telegram/Telegram-iOS/Resources/Cocoon.tgs new file mode 100644 index 0000000000000000000000000000000000000000..9eba8213addbee4b4b0205df492ea18da948319e GIT binary patch literal 2164 zcmV-)2#fb0iwFP!000021MQq!Z{xfXhW|>SYl`6T%-j|&P@pJ^ws(P1xLId6UFQJX z-`6e{``YT|3aC`cS?fA28A%o*tgBDAv1o?>P8!yIt)t(VNfJJ-Sr47~o$D z1FV;yR-5e6guQ=!$GPu5RoA8@_}Aao>np(U+YS4D#3-MuC;C`&q>uRdMO4o(TyC`U zg8tP4{kAXtk`nj_3jNS?`IPhSly1A@>wiCR@*Nv>w$T^t&W0{u`q$p04+LoZn}R_0$0{X<|KU$1qcP&(b0Mlvz3!OI z(>@mXAPzp;SY3CFh2@EfZNS>a%FQ(>Ufl>#K3jqLi}L>_c9__22l+Xh{gf!~hiz?3 z?IkyTYDU%j4Yry4-QHnyOFbyJpKiOxnqM5ZcatJ1W!^;%3vq?enDhg=pvrPcY z{)JI-;HJrGeaqcw{U83~Zr1ds-R9}zYQV<3_0O^~_tQf|{&Zi^b8NUCK~EE$U?4MmVj2dlnrz!dnHM*FPsEK55<3=6QsMCea0Q6YXvOZHXSLu9cMHYUKIr+b0Heo-1bm1Ztk6WkR)mV5Gggre}rd z*)jaU*_JZhKQPI+LpS8m{61q-N(06!1s!AeGaT*X4K+!y8e(uAvsXPg>(rv?ss}{$ zASDoKU9_1U6lVa#B=VO+A_PvrE)7m9u!S&)5YV)e?0^W>u*r(rKn4@=LNg+!je$T* z8UrAc!1XeUw`m)X5%Ge;iIjiWv80wgXI{;`vgwj|)$H!v^U8-;<5d|;YC2*v?b>me>E1gGLMqexpiq5%cIn!^ZA9hf8 zQS>uM^fRwUzcOspywM-?)^?D7BL^|krO>Z7(qd5wB$_Xk3<{xRC8$7j^wOh(Me^)r zTW!76Dv8=>`gobVL5c;Ia!4(WLbe@pIvE47j*Bj}kgFZ)!GxqUu?RpkAz+n8Tabo` z!+2R9#)#$^5_D-M+DtSOC0#uS3Z^^&O~(V!pN%rw0m>-jt)OMhK7g{3gB0;%P=>66 zXa#h@z!F$2 zsn<*?ht*nZD`Rq_(nA@blhL}@T!;!F}q6y&@{kO!c=K#GpVjfQQxtWpkZw2`w6<`)7S4)SQ)%j&F}cn-{xhvOp{&bM6Cc~!Cm#0#H+ zlz1Kwk%XXNt{DRzuEheiWNFpF@Gq2hG$#jn3U5Z)jItf?7=6J8XQoWDBZ=nRYmqEA z+bxPx8y0F~rx1Q6j%n6qfM%E$WDFw6iwOn?l#Z^s491svFd`Cq9q6|r{0?bHD-B5n z#$9A=Wd)`!d*e8q1)s%M5;%_Mmlnz<9m@=ciyXVt0GcqU!a z>3A90k`FUoX1dID>F@Ah&Q%hw@0i8O(0Dlk{;lLW)=}p`?ybt)e?sN_Qjfv?N2s~f zrR{hM*{l=^0M_vaU{PVRjb^7un&;CRmAk(_}#-sQJHmqxPq%Bz-_gMNFI qcV8TRP+`7Q7>)71^77>~tLbML?c1lS%Vzt78~uL~d4x7iTL1tEAsmGO literal 0 HcmV?d00001 diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 95f37268bc..f1b37aa6fb 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15648,3 +15648,5 @@ Error: %8$@"; "CocoonInfo.ForEveryone.Text" = "Any developer can use Cocoon for AI features. Learn more at [@cocoon](telegram) or [cocoon.org](web)."; "CocoonInfo.IntergrateInfo" = "Want to integrate Cocoon into your projects?\nReach out at [t.me/cocoon?direct]()"; "CocoonInfo.Understood" = "Understood"; + +"Conversation.Translation.CocoonInfo" = "Translations are powered by\n**#Cocoon**. [How does it work?]()"; diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 1cdc2775e3..a2b2b79f37 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -767,7 +767,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.panelButton = panelButton } - let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: params.bottomInset, innerDiameter: 52.0 * 0.5, sideInset: sideInset + 16.0) + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: params.bottomInset, innerDiameter: 52.0 * 0.5, sideInset: sideInset + 30.0) let buttonTitle: String var buttonIconName: String? diff --git a/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/ChatTranslationPanelNode.swift b/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/ChatTranslationPanelNode.swift index 9905cf01c1..56125028e5 100644 --- a/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/ChatTranslationPanelNode.swift +++ b/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/ChatTranslationPanelNode.swift @@ -17,6 +17,8 @@ import TranslateUI import TelegramUIPreferences import TelegramNotices import PremiumUI +import LocalMediaResources +import AppBundle final class ChatTranslationPanelNode: ASDisplayNode { private let context: AccountContext @@ -362,11 +364,40 @@ final class ChatTranslationPanelNode: ASDisplayNode { self?.close() }))) + items.append(.separator) + + let cocoonPath = getAppBundle().url(forResource: "Cocoon", withExtension: "tgs")?.path ?? "" + let cocoonFile = TelegramMediaFile( + fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: -123456789), + partialReference: nil, + resource: BundleResource(name: "Cocoon", path: cocoonPath), + previewRepresentations: [], + videoThumbnails: [], + immediateThumbnailData: nil, + mimeType: "application/x-tgsticker", + size: nil, + attributes: [ + .FileName(fileName: "sticker.tgs"), + .CustomEmoji(isPremium: false, isSingleColor: true, alt: "", packReference: .animatedEmojiAnimations) + ], + alternativeRepresentations: [] + ) + + let (cocoonText, entities) = parseCocoonMenuTextEntities(presentationData.strings.Conversation_Translation_CocoonInfo, emojiFileId: cocoonFile.fileId.id) + items.append(.action(ContextMenuActionItem(text: cocoonText, entities: entities, entityFiles: [cocoonFile.fileId.id: cocoonFile], enableEntityAnimations: true, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + + if let controller = self?.controller() { + let infoController = context.sharedContext.makeCocoonInfoScreen(context: context) + controller.push(infoController) + } + }))) + return ContextController.Items(content: .list(items)) } if let controller = self.controller() { - let contextController = ContextController(presentationData: presentationData, source: .reference(TranslationContextReferenceContentSource(controller: controller, sourceNode: node)), items: items, gesture: gesture) + let contextController = ContextController(context: context, presentationData: presentationData, source: .reference(TranslationContextReferenceContentSource(controller: controller, sourceNode: node)), items: items, gesture: gesture) controller.presentInGlobalOverlay(contextController) } } @@ -941,3 +972,116 @@ private final class TranslationLanguagesContextMenuContent: ContextControllerIte ) } } + +private func parseCocoonMenuTextEntities(_ input: String, emojiFileId: Int64) -> (String, [MessageTextEntity]) { + var output = "" + + var entities: [MessageTextEntity] = [] + + var i = input.startIndex + var outputCount = 0 + + func utf16Len(_ s: String) -> Int { + s.utf16.count + } + + func peek(_ offset: Int) -> Character? { + var idx = i + for _ in 0.. start { + entities.append(MessageTextEntity(range: start.. start { + entities.append(MessageTextEntity(range: start.. Date: Thu, 25 Dec 2025 01:00:37 +0400 Subject: [PATCH 5/6] Various improvements --- .../Sources/ChannelAdminController.swift | 31 +- .../WebBrowserDomainController.swift | 7 +- submodules/StatisticsUI/BUILD | 3 +- .../Sources/RevenueWithdrawalController.swift | 139 ++-- .../Sources/AlertInputFieldComponent.swift | 25 + .../Sources/AlertActionComponent.swift | 2 +- .../Sources/AlertComponent.swift | 415 +++++----- .../AlertComponent/Sources/AlertContent.swift | 16 +- .../ChatMessagePaymentAlertController.swift | 107 ++- .../Sources/FactCheckAlertController.swift | 40 +- .../Components/Gifts/GiftViewScreen/BUILD | 2 +- .../Sources/GiftPurchaseAlertController.swift | 731 ++++++------------ .../Sources/GiftViewScreen.swift | 2 +- .../Sources/GiftWithdrawAlertController.swift | 129 ++-- .../OwnershipTransferController/BUILD | 2 +- .../ChannelOwnershipTransferController.swift | 636 ++++----------- .../Sources/OwnershipTransferController.swift | 153 ++-- .../Stars/StarsWithdrawalScreen/BUILD | 1 + .../StarsRevenueWithdrawalController.swift | 131 ++-- .../Sources/StarsWithdrawalScreen.swift | 18 +- .../Sources/TelegramRootController.swift | 1 - 21 files changed, 1150 insertions(+), 1441 deletions(-) diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 96e2860b9d..57a6e9a354 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -1104,19 +1104,30 @@ public func channelAdminController(context: AccountContext, updatedPresentationD } transferOwnershipDisposable.set((context.engine.peers.checkOwnershipTranfserAvailability(memberId: adminId) |> deliverOnMainQueue).start(error: { error in - let controller = channelOwnershipTransferController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, member: member, initialError: error, present: { c, a in - presentControllerImpl?(c, a) - }, completion: { upgradedPeerId in - if let upgradedPeerId = upgradedPeerId { - upgradedToSupergroupImpl(upgradedPeerId, { + let controller = channelOwnershipTransferController( + context: context, + updatedPresentationData: updatedPresentationData, + peer: peer, + member: member, + initialError: error, + present: { c, a in + presentControllerImpl?(c, a) + }, + push: { c in + pushControllerImpl?(c) + }, + completion: { upgradedPeerId in + if let upgradedPeerId = upgradedPeerId { + upgradedToSupergroupImpl(upgradedPeerId, { + dismissImpl?() + transferedOwnership(member.id) + }) + } else { dismissImpl?() transferedOwnership(member.id) - }) - } else { - dismissImpl?() - transferedOwnership(member.id) + } } - }) + ) presentControllerImpl?(controller, nil) })) }) diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift index 88a33bcdba..d75618da14 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift @@ -22,8 +22,7 @@ public func webBrowserDomainController(context: AccountContext, updatedPresentat return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - let doneInProgressValuePromise = ValuePromise(false) - let doneInProgress = doneInProgressValuePromise.get() + let doneInProgressPromise = ValuePromise(false) var content: [AnyComponentWithIdentity] = [] content.append(AnyComponentWithIdentity( @@ -89,14 +88,14 @@ public func webBrowserDomainController(context: AccountContext, updatedPresentat .init(title: strings.Common_Cancel), .init(title: strings.Common_Done, type: .default, action: { applyImpl?() - }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgress) + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get()) ], updatedPresentationData: effectiveUpdatedPresentationData ) applyImpl = { let updatedLink = explicitUrl(inputState.value) if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) { - doneInProgressValuePromise.set(true) + doneInProgressPromise.set(true) apply(updatedLink) } else { inputState.animateError() diff --git a/submodules/StatisticsUI/BUILD b/submodules/StatisticsUI/BUILD index 293b755d92..f5dbe1a187 100644 --- a/submodules/StatisticsUI/BUILD +++ b/submodules/StatisticsUI/BUILD @@ -49,13 +49,14 @@ swift_library( "//submodules/TelegramNotices", "//submodules/UIKitRuntimeUtils", "//submodules/PasswordSetupUI", - "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", "//submodules/TelegramUI/Components/ListItemComponentAdaptor", "//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", "//submodules/TelegramUI/Components/PremiumPeerShortcutComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift b/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift index 3a9deb4779..e6abc59776 100644 --- a/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift +++ b/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift @@ -6,71 +6,108 @@ import TelegramPresentationData import PresentationDataUtils import AccountContext import PasswordSetupUI -import Markdown -import OwnershipTransferController +import ComponentFlow import AlertComponent +import AlertInputFieldComponent -func confirmRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } +func confirmRevenueWithdrawalController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + peerId: EnginePeer.Id, + present: @escaping (ViewController, Any?) -> Void, + completion: @escaping (String) -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let inputState = AlertInputFieldComponent.ExternalState() + + let doneIsEnabled: Signal = inputState.valueSignal + |> map { value in + return !value.isEmpty + } + + let doneInProgressPromise = ValuePromise(false) + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Monetization_Withdraw_EnterPassword_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Monetization_Withdraw_EnterPassword_Text)) + ) + )) + + var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + placeholder: strings.Channel_OwnershipTransfer_PasswordPlaceholder, + isSecureTextEntry: true, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) + } var dismissImpl: (() -> Void)? - var proceedImpl: (() -> Void)? - - let disposable = MetaDisposable() - - let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Title, text: presentationData.strings.Monetization_Withdraw_EnterPassword_Text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?() - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Done, action: { - proceedImpl?() - })]) - - contentNode.complete = { - proceedImpl?() - } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.theme = presentationData.theme - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - disposable.dispose() - } - dismissImpl = { [weak controller, weak contentNode] in - contentNode?.dismissInput() - controller?.dismissAnimated() - } - proceedImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - contentNode.updateIsChecking(true) - - let signal = context.engine.peers.requestStarsRevenueWithdrawalUrl(peerId: peerId, ton: true, amount: nil, password: contentNode.password) - disposable.set((signal |> deliverOnMainQueue).start(next: { url in + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Monetization_Withdraw_EnterPassword_Done, type: .default, action: { + applyImpl?() + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get()) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + doneInProgressPromise.set(true) + + let _ = (context.engine.peers.requestStarsRevenueWithdrawalUrl(peerId: peerId, ton: true, amount: nil, password: inputState.value) + |> deliverOnMainQueue).start(next: { url in dismissImpl?() completion(url) - }, error: { [weak contentNode] error in + }, error: { error in var errorTextAndActions: (String, [TextAlertAction])? switch error { - case .invalidPassword: - contentNode?.animateError() - case .limitExceeded: - errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - default: - errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + case .invalidPassword: + inputState.animateError() + case .limitExceeded: + errorTextAndActions = (strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) + default: + errorTextAndActions = (strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) } - contentNode?.updateIsChecking(false) - + doneInProgressPromise.set(false) + if let (text, actions) = errorTextAndActions { dismissImpl?() present(textAlertController(context: context, title: nil, text: text, actions: actions), nil) } - })) + }) } - - return controller + dismissImpl = { [weak alertController] in + alertController?.dismiss(completion: nil) + } + return alertController } diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift index 902c0d20ff..34140adfa3 100644 --- a/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift @@ -140,11 +140,16 @@ public final class AlertInputFieldComponent: Component { return self.textField.text ?? "" } + private var clearOnce: Bool = false + func activateInput() { self.textField.becomeFirstResponder() } func animateError() { + if let component = self.component, component.isInitiallyFocused { + self.clearOnce = true + } self.textField.layer.addShakeAnimation() HapticFeedback().error() @@ -169,6 +174,26 @@ public final class AlertInputFieldComponent: Component { self.clearButton.view?.isHidden = true } + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + guard let component = self.component else { + return true + } + + if self.clearOnce { + self.clearOnce = false + if range.length > string.count { + textField.text = "" + return false + } + } + + let updatedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + if let shouldChangeText = component.shouldChangeText { + return shouldChangeText(updatedText) + } + return true + } + public func setText(text: String) { self.textField.text = text if !self.isUpdating { diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift index 30cbac294a..97c171af9d 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift @@ -135,7 +135,7 @@ final class AlertActionComponent: Component { let titlePadding: CGFloat = 16.0 let titleSize = self.title.update( - transition: transition, + transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(attributedString), horizontalAlignment: .center, diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift index efc2346143..212653869c 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -38,14 +38,14 @@ private final class AlertScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let configuration: AlertScreen.Configuration - let content: [AnyComponentWithIdentity] - let actions: [AlertScreen.Action] + let content: Signal<[AnyComponentWithIdentity], NoError> + let actions: Signal<[AlertScreen.Action], NoError> let ready: Promise init( configuration: AlertScreen.Configuration, - content: [AnyComponentWithIdentity], - actions: [AlertScreen.Action], + content: Signal<[AnyComponentWithIdentity], NoError>, + actions: Signal<[AlertScreen.Action], NoError>, ready: Promise ) { self.configuration = configuration @@ -72,8 +72,12 @@ private final class AlertScreenComponent: Component { private let containerView = GlassBackgroundContainerView() private let backgroundView = GlassBackgroundView() - private var content: [AnyHashable: ComponentView] = [:] - private var actions: [AnyHashable: ComponentView] = [:] + private var disposable: Disposable? + private var content: [AnyComponentWithIdentity]? + private var actions: [AlertScreen.Action]? + + private var contentItems: [AnyHashable: ComponentView] = [:] + private var actionItems: [AnyHashable: ComponentView] = [:] private var highlightedAction: AnyHashable? private let hapticFeedback = HapticFeedback() @@ -122,10 +126,14 @@ private final class AlertScreenComponent: Component { preconditionFailure() } + deinit { + self.disposable?.dispose() + } + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is ActionSelectionGestureRecognizer { let location = gestureRecognizer.location(in: self.backgroundView) - for (_, action) in self.actions { + for (_, action) in self.actionItems { if let actionView = action.view, actionView.frame.contains(location) { return true } @@ -141,7 +149,7 @@ private final class AlertScreenComponent: Component { switch gestureRecognizer.state { case .began, .changed: var highlightedActionId: AnyHashable? - for (actionId, action) in self.actions { + for (actionId, action) in self.actionItems { if let actionView = action.view, actionView.frame.contains(location) { highlightedActionId = actionId break @@ -243,24 +251,24 @@ private final class AlertScreenComponent: Component { } func updateActionHighlight(previous: Bool) { - guard let component = self.component else { + guard let actions = self.actions else { return } guard let highlightedAction = self.highlightedAction else { - if let action = component.actions.first(where: { $0.type == .default }) { + if let action = actions.first(where: { $0.type == .default }) { self.highlightedAction = action.id - } else if let action = component.actions.first(where: { $0.type == .defaultDestructive }) { + } else if let action = actions.first(where: { $0.type == .defaultDestructive }) { self.highlightedAction = action.id - } else if case .verticalReversed = self.effectiveActionLayout, let action = component.actions.last { + } else if case .verticalReversed = self.effectiveActionLayout, let action = actions.last { self.highlightedAction = action.id - } else if let action = component.actions.first { + } else if let action = actions.first { self.highlightedAction = action.id } self.state?.updated(transition: .easeInOut(duration: 0.2)) return } - let sequence = previous ? component.actions.reversed() : component.actions + let sequence = previous ? actions.reversed() : actions var selectNext = false var newHighlightedAction: AnyHashable? @@ -281,13 +289,13 @@ private final class AlertScreenComponent: Component { } func performHighlightedAction() { - guard let component = self.component else { + guard let actions = self.actions else { return } guard let highlightedAction = self.highlightedAction else { return } - guard let action = component.actions.first(where: { AnyHashable($0.id) == highlightedAction }) else { + guard let action = actions.first(where: { AnyHashable($0.id) == highlightedAction }) else { return } action.action() @@ -305,6 +313,25 @@ private final class AlertScreenComponent: Component { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment self.state = state + + if self.component == nil { + self.disposable = (combineLatest( + queue: Queue.mainQueue(), + component.content, + component.actions + ) |> deliverOnMainQueue).start(next: { [weak self] content, actions in + guard let self else { + return + } + self.content = content + self.actions = actions + + if !self.isUpdating { + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + }) + } + self.component = component var alertHeight: CGFloat = 0.0 @@ -322,41 +349,43 @@ private final class AlertScreenComponent: Component { 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 + if let content = self.content { + for content in content { + if contentOriginY.isZero { + contentOriginY += contentTopInset + } else { + contentOriginY += contentSpacing } - 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) - item.parentState = state + validContentIds.insert(content.id) + + let item: ComponentView + var itemTransition = transition + if let current = self.contentItems[content.id] { + item = current + } else { + item = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.contentItems[content.id] = item } - transition.setFrame(view: itemView, frame: itemFrame) + + 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) + item.parentState = state + } + transition.setFrame(view: itemView, frame: itemFrame) + } + contentOriginY += itemSize.height } - contentOriginY += itemSize.height } if !contentOriginY.isZero { @@ -364,145 +393,150 @@ private final class AlertScreenComponent: Component { 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.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.85), - 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 - } else if component.actions.count == 1 { - 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, - isEnabled: action.isEnabled, - progress: action.progress - )), - environment: { alertEnvironment }, - containerSize: fullWidthActionSize + if let actions = self.actions { + let genericActionTheme = AlertActionComponent.Theme( + background: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), + foreground: environment.theme.actionSheet.primaryTextColor, + secondary: environment.theme.actionSheet.secondaryTextColor, + font: .regular ) - if let itemView = item.view { - if itemView.superview == nil { - self.backgroundView.contentView.addSubview(itemView) + let defaultActionTheme = AlertActionComponent.Theme( + background: environment.theme.actionSheet.controlAccentColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + secondary: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.85), + 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 + } else if actions.count == 1 { + effectiveActionLayout = .vertical + } + var actionTransitions: [AnyHashable: ComponentTransition] = [:] + var validActionIds: Set = Set() + for action in actions { + validActionIds.insert(action.id) + + let item: ComponentView + var itemTransition = transition + if let current = self.actionItems[action.id] { + item = current + } else { + item = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.actionItems[action.id] = item + } + actionTransitions[action.id] = itemTransition + + 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, + isEnabled: action.isEnabled, + progress: action.progress + )), + 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 case .horizontal = effectiveActionLayout, itemSize.width > halfWidthActionSize.width { - effectiveActionLayout = .verticalReversed + if !actions.isEmpty { + let actionsHeight: CGFloat + if self.effectiveActionLayout.isVertical { + actionsHeight = fullWidthActionSize.height * CGFloat(actions.count) + actionSpacing * CGFloat(actions.count - 1) + } else { + actionsHeight = fullWidthActionSize.height + } + alertHeight += actionsHeight + alertHeight += actionSideInset } - } - 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 + + var actionOriginX: CGFloat = actionSideInset + var actionOriginY: CGFloat switch self.effectiveActionLayout { - case .horizontal: - itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: halfWidthActionSize) - actionOriginX += halfWidthActionSize.width + actionSpacing + case .horizontal, .verticalReversed: + actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height 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 + actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height * CGFloat( actions.count) - actionSpacing * CGFloat(actions.count - 1) } - 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 + for action in actions { + guard let item = self.actionItems[action.id], let itemView = item.view as? AlertActionComponent.View else { + continue + } + let itemTransition = actionTransitions[action.id] ?? transition + 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: itemTransition) + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + var removeActionIds: [AnyHashable] = [] + for (id, item) in self.actionItems { + 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() - }) - } else { - itemView.removeFromSuperview() + } } } } - } - for id in removeActionIds { - self.actions.removeValue(forKey: id) + for id in removeActionIds { + self.actionItems.removeValue(forKey: id) + } } let alertSize = CGSize(width: alertWidth, height: alertHeight) @@ -570,6 +604,7 @@ open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { public let progress: Signal public init( + id: AnyHashable? = nil, title: String, type: ActionType = .generic, action: @escaping () -> Void = {}, @@ -583,6 +618,12 @@ open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { self.autoDismiss = autoDismiss self.isEnabled = isEnabled self.progress = progress + + if let id { + self.id = id + } else { + self.id = title + } } public static func ==(lhs: Action, rhs: Action) -> Bool { @@ -598,7 +639,7 @@ open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { return true } - fileprivate let id: Int64 = Int64.random(in: Int64.min ..< Int64.max) + fileprivate let id: AnyHashable } private var processedDidAppear: Bool = false @@ -613,8 +654,8 @@ open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { public init( configuration: Configuration = Configuration(), - content: [AnyComponentWithIdentity], - actions: [Action], + contentSignal: Signal<[AnyComponentWithIdentity], NoError>, + actionsSignal: Signal<[Action], NoError>, updatedPresentationData: (initial: PresentationData, signal: Signal) ) { let componentReady = Promise() @@ -622,8 +663,8 @@ open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { super.init( component: AlertScreenComponent( configuration: configuration, - content: content, - actions: actions, + content: contentSignal, + actions: actionsSignal, ready: componentReady ), navigationBarAppearance: .none, @@ -636,6 +677,20 @@ open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { //self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true))) } + public convenience init( + configuration: Configuration = Configuration(), + content: [AnyComponentWithIdentity], + actions: [Action], + updatedPresentationData: (initial: PresentationData, signal: Signal) + ) { + self.init( + configuration: configuration, + contentSignal: .single(content), + actionsSignal: .single(actions), + updatedPresentationData: updatedPresentationData + ) + } + public convenience init( context: AccountContext, configuration: Configuration = Configuration(), diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift index 889d8a75d8..8e68f3a1e6 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift @@ -165,6 +165,7 @@ public final class AlertTextComponent: Component { let alignment: Alignment let color: Color let style: Style + let insets: UIEdgeInsets let action: ([NSAttributedString.Key: Any]) -> Void public init( @@ -172,12 +173,14 @@ public final class AlertTextComponent: Component { alignment: Alignment = .default, color: Color = .primary, style: Style = .plain(.default), + insets: UIEdgeInsets = .zero, action: @escaping ([NSAttributedString.Key: Any]) -> Void = { _ in } ) { self.content = content self.alignment = alignment self.color = color self.style = style + self.insets = insets self.action = action } @@ -188,10 +191,13 @@ public final class AlertTextComponent: Component { if lhs.alignment != rhs.alignment { return false } + if lhs.color != rhs.color { + return false + } if lhs.style != rhs.style { return false } - if lhs.color != rhs.color { + if lhs.insets != rhs.insets { return false } return true @@ -215,7 +221,7 @@ public final class AlertTextComponent: Component { case .primary: textColor = environment.theme.actionSheet.primaryTextColor case .secondary: - textColor = environment.theme.actionSheet.secondaryTextColor + textColor = environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.35) case .destructive: textColor = environment.theme.actionSheet.destructiveActionTextColor } @@ -330,7 +336,7 @@ public final class AlertTextComponent: Component { environment: {}, containerSize: backgroundSize ) - let backgroundFrame = CGRect(origin: CGPoint(x: -10.0, y: 0.0), size: backgroundSize) + let backgroundFrame = CGRect(origin: CGPoint(x: -10.0, y: component.insets.top), size: backgroundSize) if let backgroundView = self.background.view { if backgroundView.superview == nil { self.addSubview(backgroundView) @@ -339,14 +345,14 @@ public final class AlertTextComponent: Component { } } - let textFrame = CGRect(origin: textOffset, size: textSize) + let textFrame = CGRect(origin: textOffset.offsetBy(dx: 0.0, dy: component.insets.top), size: textSize) if let textView = self.text.view { if textView.superview == nil { self.addSubview(textView) } transition.setFrame(view: textView, frame: textFrame) } - return size + return CGSize(width: size.width, height: size.height + component.insets.top + component.insets.bottom) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift index 3bb04f3f39..9f6ac88ecb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift @@ -28,20 +28,66 @@ public class ChatMessagePaymentAlertController: AlertScreen { private let animateBalanceOverlay: Bool private var didUpdateCurrency = false - public var currency: CurrencyAmount.Currency { - didSet { - self.didUpdateCurrency = true - if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut)) - } - } - } - + + private var initialCurrency: CurrencyAmount.Currency? + public var currency: CurrencyAmount.Currency? + private var currencyDisposable: Disposable? + private let balance = ComponentView() private var didAppear = false public init( + context: AccountContext?, + presentationData: PresentationData, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + configuration: Configuration = AlertScreen.Configuration(), + contentSignal: Signal<[AnyComponentWithIdentity], NoError>, + actionsSignal: Signal<[AlertScreen.Action], NoError>, + navigationController: NavigationController?, + chatPeerId: EnginePeer.Id, + showBalance: Bool = true, + currencySignal: Signal = .single(.stars), + animateBalanceOverlay: Bool = true + ) { + self.context = context + self.presentationData = presentationData + self.parentNavigationController = navigationController + self.chatPeerId = chatPeerId + self.showBalance = showBalance + self.animateBalanceOverlay = animateBalanceOverlay + + var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (initial: presentationData, signal: .single(presentationData)) + } + + super.init( + configuration: configuration, + contentSignal: contentSignal, + actionsSignal: actionsSignal, + updatedPresentationData: effectiveUpdatedPresentationData + ) + + self.currencyDisposable = (currencySignal + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] currency in + guard let self else { + return + } + if self.currency == nil { + self.initialCurrency = currency + } + self.currency = currency + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + }) + } + + public convenience init( context: AccountContext?, presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, @@ -54,26 +100,18 @@ public class ChatMessagePaymentAlertController: AlertScreen { currency: CurrencyAmount.Currency = .stars, animateBalanceOverlay: Bool = true ) { - self.context = context - self.presentationData = presentationData - self.parentNavigationController = navigationController - self.chatPeerId = chatPeerId - self.showBalance = showBalance - self.currency = currency - self.animateBalanceOverlay = animateBalanceOverlay - - var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal) - if let updatedPresentationData { - effectiveUpdatedPresentationData = updatedPresentationData - } else { - effectiveUpdatedPresentationData = (initial: presentationData, signal: .single(presentationData)) - } - - super.init( + self.init( + context: context, + presentationData: presentationData, + updatedPresentationData: updatedPresentationData, configuration: configuration, - content: content, - actions: actions, - updatedPresentationData: effectiveUpdatedPresentationData + contentSignal: .single(content), + actionsSignal: .single(actions), + navigationController: navigationController, + chatPeerId: chatPeerId, + showBalance: showBalance, + currencySignal: .single(currency), + animateBalanceOverlay: animateBalanceOverlay ) } @@ -89,8 +127,11 @@ public class ChatMessagePaymentAlertController: AlertScreen { private func animateOut() { if !self.animateBalanceOverlay { - if self.currency == .ton && self.didUpdateCurrency { + if case .ton = self.currency, let initialCurrency, initialCurrency != self.currency { self.currency = .stars + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut)) + } } } else { if let view = self.balance.view { @@ -112,7 +153,7 @@ public class ChatMessagePaymentAlertController: AlertScreen { } } - if let context = self.context, let _ = self.parentNavigationController, self.showBalance { + if let context = self.context, let _ = self.parentNavigationController, self.showBalance, let currency = self.currency { let insets = layout.insets(options: .statusBar) var balanceTransition = ComponentTransition(transition) if self.balance.view == nil { @@ -126,12 +167,12 @@ public class ChatMessagePaymentAlertController: AlertScreen { context: context, peerId: self.chatPeerId.namespace == Namespaces.Peer.CloudChannel ? self.chatPeerId : context.account.peerId, theme: self.presentationData.theme, - currency: self.currency, + currency: currency, action: { [weak self] in - guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController else { + guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController, let currency = self.currency else { return } - switch self.currency { + switch currency { case .stars: let _ = (context.engine.payments.starsTopUpOptions() |> take(1) diff --git a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift index 9916fdfe22..fb4c4a6c0f 100644 --- a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift @@ -73,23 +73,45 @@ public func factCheckAlertController( ) )) + let doneIsRemove: Signal + if !value.isEmpty { + doneIsRemove = inputState.valueSignal + |> map { value in + return value.string.isEmpty + } + |> distinctUntilChanged + } else { + doneIsRemove = .single(false) + } + + let actionsSignal: Signal<[AlertScreen.Action], NoError> = doneIsRemove + |> map { doneIsRemove in + var actions: [AlertScreen.Action] = [] + actions.append(.init(title: strings.Common_Cancel)) + + let doneTitle: String = doneIsRemove ? strings.FactCheck_Remove : strings.Common_Done + let doneType: AlertScreen.Action.ActionType = doneIsRemove ? .defaultDestructive : .default + actions.append( + .init(id: "done", title: doneTitle, type: doneType, action: { + let (text, entities) = inputState.textAndEntities + apply(text, entities) + }, isEnabled: doneIsEnabled) + ) + + return actions + } + var effectiveUpdatedPresentationData: (PresentationData, Signal) if let updatedPresentationData { effectiveUpdatedPresentationData = updatedPresentationData } else { effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } - //FactCheck_Remove + let alertController = AlertScreen( configuration: AlertScreen.Configuration(allowInputInset: true), - content: content, - actions: [ - .init(title: strings.Common_Cancel), - .init(title: strings.Common_Done, type: .default, action: { - let (text, entities) = inputState.textAndEntities - apply(text, entities) - }, isEnabled: doneIsEnabled) - ], + contentSignal: .single(content), + actionsSignal: actionsSignal, updatedPresentationData: effectiveUpdatedPresentationData ) presentImpl = { [weak alertController] c in diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 65f74cb098..5d86b40c9c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -46,7 +46,6 @@ swift_library( "//submodules/MoreButtonNode", "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/PasswordSetupUI", - "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", "//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen", @@ -70,6 +69,7 @@ swift_library( "//submodules/TelegramUI/Components/AvatarComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertTableComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift index 4d1729352b..d2cc73af32 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift @@ -11,440 +11,16 @@ import TelegramUIPreferences import AccountContext import AppBundle import AvatarNode -import Markdown import GiftItemComponent import ChatMessagePaymentAlertController -import ActivityIndicator import TabSelectorComponent import BundleIconComponent import MultilineTextComponent import TelegramStringFormatting import TooltipUI - -private final class GiftPurchaseAlertContentNode: AlertContentNode { - private let context: AccountContext - private let strings: PresentationStrings - private var presentationTheme: PresentationTheme - private let gift: StarGift.UniqueGift - private let peer: EnginePeer - - fileprivate var currency: CurrencyAmount.Currency - - fileprivate let header = ComponentView() - private let title = ComponentView() - private let text = ComponentView() - private let giftView = ComponentView() - private let arrow = ComponentView() - private let avatarNode: AvatarNode - - 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) - } - } - } - - var updatedCurrency: (CurrencyAmount.Currency) -> Void = { _ in } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init( - context: AccountContext, - theme: AlertControllerTheme, - presentationTheme: PresentationTheme, - strings: PresentationStrings, - gift: StarGift.UniqueGift, - peer: EnginePeer, - actions: [TextAlertAction] - ) { - self.context = context - self.strings = strings - self.presentationTheme = presentationTheme - self.gift = gift - self.peer = peer - - 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 - - self.currency = self.gift.resellForTonOnly ? .ton : .stars - - super.init() - - 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: presentationTheme, peer: peer) - } - - override func updateTheme(_ theme: AlertControllerTheme) { - 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) - } - } - - func requestUpdate(transition: ContainedViewLayoutTransition) { - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: transition) - } - } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let containerSize = size - var size = size - size.width = min(size.width, 270.0) - - var origin = CGPoint(x: 0.0, y: 20.0) - if self.gift.resellForTonOnly { - let headerSize = self.header.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: self.strings.Gift_Buy_AcceptsTonOnly, font: Font.regular(13.0), textColor: self.presentationTheme.actionSheet.secondaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 2 - ) - ), - environment: {}, - containerSize: CGSize(width: size.width - 32.0, height: size.height) - ) - - let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerSize.width) / 2.0), y: origin.y), size: headerSize) - if let view = self.header.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.frame = headerFrame - } - origin.y += headerSize.height + 17.0 - } else { - origin.y -= 4.0 - - let headerSize = self.header.update( - transition: ComponentTransition(transition), - component: AnyComponent(TabSelectorComponent( - colors: TabSelectorComponent.Colors( - foreground: self.presentationTheme.list.itemSecondaryTextColor, - selection: self.presentationTheme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15), - simple: true - ), - theme: self.presentationTheme, - customLayout: TabSelectorComponent.CustomLayout( - font: Font.medium(14.0), - spacing: 10.0 - ), - items: [ - TabSelectorComponent.Item( - id: AnyHashable(0), - content: .text(self.strings.Gift_Buy_PayInStars) - ), - TabSelectorComponent.Item( - id: AnyHashable(1), - content: .text(self.strings.Gift_Buy_PayInTon) - ) - ], - selectedId: self.currency == .ton ? AnyHashable(1) : AnyHashable(0), - setSelectedId: { [weak self] id in - guard let self else { - return - } - let currency: CurrencyAmount.Currency - if id == AnyHashable(0) { - currency = .stars - } else { - currency = .ton - } - if self.currency != currency { - self.currency = currency - self.updatedCurrency(currency) - self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) - } - } - )), - environment: {}, - containerSize: CGSize(width: containerSize.width - 16.0 * 2.0, height: 100.0) - ) - - size.width = min(containerSize.width, max(270.0, headerSize.width + 32.0)) - - let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerSize.width) / 2.0), y: origin.y), size: headerSize) - if let view = self.header.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.frame = headerFrame - } - origin.y += headerSize.height + 17.0 - } - - self.validLayout = size - - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - - var resellPrice: CurrencyAmount? - if let actionNode = self.actionNodes.first { - switch self.currency { - case .stars: - if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .stars }) { - resellPrice = resellAmount - actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyFor(Int32(resellAmount.amount.value)), action: actionNode.action.action) - } - case .ton: - if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .ton }) { - resellPrice = resellAmount - let valueString = formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat) - actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyForTon(valueString).string, action: actionNode.action.action) - } - } - } - - 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: self.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 - } - - let arrowSize = self.arrow.update( - transition: .immediate, - component: AnyComponent(BundleIconComponent(name: "Peer Info/AlertArrow", tintColor: self.presentationTheme.actionSheet.secondaryTextColor)), - environment: {}, - containerSize: size - ) - let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowSize.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowSize.height) / 2.0)), size: arrowSize) - if let view = self.arrow.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.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.title.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: self.strings.Gift_Buy_Confirm_Title, font: Font.semibold(17.0), textColor: self.presentationTheme.actionSheet.primaryTextColor)), - horizontalAlignment: .center - ) - ), - environment: { - }, - containerSize: CGSize(width: size.width - 32.0, height: size.height) - ) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize) - if let view = self.title.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.frame = titleFrame - } - origin.y += titleSize.height + 5.0 - - let giftTitle = "\(self.gift.title) #\(presentationStringsFormattedNumber(self.gift.number, presentationData.dateTimeFormat.groupingSeparator))" - - let priceString: String - if let resellPrice { - switch resellPrice.currency { - case .stars: - priceString = self.strings.Gift_Buy_Confirm_Text_Stars(Int32(clamping: resellPrice.amount.value)) - case .ton: - priceString = "**\(formatTonAmountText(resellPrice.amount.value, dateTimeFormat: presentationData.dateTimeFormat)) TON**" - } - } else { - priceString = "" - } - - let text: String - if self.peer.id == self.context.account.peerId { - text = self.strings.Gift_Buy_Confirm_Text(giftTitle, priceString).string - } else { - text = self.strings.Gift_Buy_Confirm_GiftText(giftTitle, priceString, self.peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string - } - - let textSize = self.text.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .markdown(text: text, attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationTheme.actionSheet.primaryTextColor), - bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: self.presentationTheme.actionSheet.primaryTextColor), - link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationTheme.actionSheet.primaryTextColor), - linkAttribute: { url in - return ("URL", url) - } - )), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - ) - ), - environment: { - }, - containerSize: CGSize(width: size.width - 32.0, height: size.height) - ) - let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize) - if let view = self.text.view { - if view.superview == nil { - self.view.addSubview(view) - } - view.frame = textFrame - } - 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 resultSize = CGSize(width: contentWidth, height: origin.y + actionsHeight - 26.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:*/ - 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 - /*switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical:*/ - do { - 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:*/ - 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 AlertTransferHeaderComponent +import AvatarComponent public func giftPurchaseAlertController( context: AccountContext, @@ -454,73 +30,256 @@ public func giftPurchaseAlertController( navigationController: NavigationController?, commit: @escaping (CurrencyAmount.Currency) -> Void, dismissed: @escaping () -> Void -) -> AlertController { +) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - - var contentNode: GiftPurchaseAlertContentNode? - var dismissImpl: ((Bool) -> Void)? - var commitImpl: (() -> Void)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "", action: { - commitImpl?() - dismissImpl?(true) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - })] - contentNode = GiftPurchaseAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), presentationTheme: presentationData.theme, strings: strings, gift: gift, peer: peer, actions: actions) - -// let controller = ChatMessagePaymentAlertController( -// context: context, -// presentationData: presentationData, -// contentNode: contentNode!, -// navigationController: navigationController, -// chatPeerId: context.account.peerId, -// showBalance: true, -// currency: gift.resellForTonOnly ? .ton : .stars, -// animateBalanceOverlay: animateBalanceOverlay -// ) - - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) - - controller.dismissed = { _ in - dismissed() + let currencyPromise = ValuePromise(.stars) + if gift.resellForTonOnly { + currencyPromise.set(.ton) } - - dismissImpl = { [weak controller] animated in - if animated { - controller?.dismissAnimated() + + let contentSignal = currencyPromise.get() + |> map { currency in + var content: [AnyComponentWithIdentity] = [] + if gift.resellForTonOnly { + content.append(AnyComponentWithIdentity( + id: "tonOnly", + component: AnyComponent( + AlertTextComponent( + content: .plain(strings.Gift_Buy_AcceptsTonOnly), + alignment: .center, + color: .secondary, + style: .plain(.small), + insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 8.0, right: 0.0) + ) + ) + )) } else { - controller?.dismiss() + content.append(AnyComponentWithIdentity( + id: "currency", + component: AnyComponent( + AlertCurrencyComponent( + currency: currency, + updatedCurrency: { currency in + currencyPromise.set(currency) + } + ) + ) + )) } - } - commitImpl = { [weak contentNode] in - contentNode?.inProgress = true - commit(contentNode?.currency ?? .stars) - } - - contentNode?.updatedCurrency = { [weak controller] currency in - let _ = controller - //controller?.currency = currency - } - - if !gift.resellForTonOnly { - Queue.mainQueue().after(0.3) { - if let headerView = contentNode?.header.view as? TabSelectorComponent.View { - let absoluteFrame = headerView.convert(headerView.bounds, to: nil) - var originX = absoluteFrame.width * 0.75 - if let itemFrame = headerView.frameForItem(AnyHashable(1)) { - originX = itemFrame.midX - } - let location = CGRect(origin: CGPoint(x: absoluteFrame.minX + floor(originX), y: absoluteFrame.minY - 8.0), size: CGSize()) - let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: presentationData.strings.Gift_Buy_PayInTon_Tooltip), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in - return .dismiss(consume: false) - }) - controller.present(tooltipController, in: .window(.root)) + + 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 + ) + )), + type: .transfer + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Gift_Buy_Confirm_Title) + ) + )) + + let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))" + var priceString = "" + switch currency { + case .stars: + if let resellAmount = gift.resellAmounts?.first(where: { $0.currency == .stars }) { + priceString = strings.Gift_Buy_Confirm_Text_Stars(Int32(clamping: resellAmount.amount.value)) + } + case .ton: + if let resellAmount = gift.resellAmounts?.first(where: { $0.currency == .ton }) { + priceString = "**\(formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat)) TON**" } } + + let text: String + if peer.id == context.account.peerId { + text = strings.Gift_Buy_Confirm_Text(giftTitle, priceString).string + } else { + text = strings.Gift_Buy_Confirm_GiftText(giftTitle, priceString, peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string + } + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + return content } - return controller + let actionsSignal = currencyPromise.get() + |> map { currency in + var actions: [AlertScreen.Action] = [] + var buyString = "" + switch currency { + case .stars: + if let resellAmount = gift.resellAmounts?.first(where: { $0.currency == .stars }) { + buyString = strings.Gift_Buy_Confirm_BuyFor(Int32(resellAmount.amount.value)) + } + case .ton: + if let resellAmount = gift.resellAmounts?.first(where: { $0.currency == .ton }) { + buyString = strings.Gift_Buy_Confirm_BuyForTon(formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat)).string + } + } + actions.append(.init(id: "buy", title: buyString, type: .default, action: { + commit(currency) + })) + actions.append(.init(title: strings.Common_Cancel)) + return actions + } + + let alertController = ChatMessagePaymentAlertController( + context: context, + presentationData: presentationData, + updatedPresentationData: (presentationData, context.sharedContext.presentationData), + configuration: AlertScreen.Configuration(actionAlignment: .vertical, dismissOnOutsideTap: true, allowInputInset: false), + contentSignal: contentSignal, + actionsSignal: actionsSignal, + navigationController: navigationController, + chatPeerId: context.account.peerId, + showBalance: true, + currencySignal: currencyPromise.get(), + animateBalanceOverlay: animateBalanceOverlay + ) + alertController.dismissed = { _ in + dismissed() + } + +// if !gift.resellForTonOnly { +// Queue.mainQueue().after(0.3) { +// if let headerView = contentNode?.header.view as? TabSelectorComponent.View { +// let absoluteFrame = headerView.convert(headerView.bounds, to: nil) +// var originX = absoluteFrame.width * 0.75 +// if let itemFrame = headerView.frameForItem(AnyHashable(1)) { +// originX = itemFrame.midX +// } +// let location = CGRect(origin: CGPoint(x: absoluteFrame.minX + floor(originX), y: absoluteFrame.minY - 8.0), size: CGSize()) +// let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: presentationData.strings.Gift_Buy_PayInTon_Tooltip), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in +// return .dismiss(consume: false) +// }) +// controller.present(tooltipController, in: .window(.root)) +// } +// } +// } + + return alertController +} + +private final class AlertCurrencyComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + let currency: CurrencyAmount.Currency + let updatedCurrency: (CurrencyAmount.Currency) -> Void + + public init( + currency: CurrencyAmount.Currency, + updatedCurrency: @escaping (CurrencyAmount.Currency) -> Void + ) { + self.currency = currency + self.updatedCurrency = updatedCurrency + } + + public static func ==(lhs: AlertCurrencyComponent, rhs: AlertCurrencyComponent) -> Bool { + if lhs.currency != rhs.currency { + return false + } + return true + } + + final class View: UIView { + private let header = ComponentView() + + private var component: AlertCurrencyComponent? + private weak var state: EmptyComponentState? + + func update(component: AlertCurrencyComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let headerSize = self.header.update( + transition: transition, + component: AnyComponent(TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.35), + selection: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), + simple: true + ), + theme: environment.theme, + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: 10.0 + ), + items: [ + TabSelectorComponent.Item( + id: AnyHashable(0), + content: .text(environment.strings.Gift_Buy_PayInStars) + ), + TabSelectorComponent.Item( + id: AnyHashable(1), + content: .text(environment.strings.Gift_Buy_PayInTon) + ) + ], + selectedId: component.currency == .ton ? AnyHashable(1) : AnyHashable(0), + setSelectedId: { [weak self] id in + guard let self, let component = self.component else { + return + } + let currency: CurrencyAmount.Currency + if id == AnyHashable(0) { + currency = .stars + } else { + currency = .ton + } + if currency != component.currency { + component.updatedCurrency(currency) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width + 54.0, height: 100.0) + ) + + let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - headerSize.width) / 2.0), y: 0.0), size: headerSize) + if let view = self.header.view { + if view.superview == nil { + self.addSubview(view) + } + view.frame = headerFrame + } + + return CGSize(width: availableSize.width, height: headerSize.height + 12.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/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 774ce219f0..88e65ee1e1 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -3071,7 +3071,7 @@ private final class GiftViewSheetContent: CombinedComponent { i += 1 } } - //TODO:localize + var buttonColor: UIColor = UIColor.white.withAlphaComponent(0.16) if let previewPatternColor = giftCompositionExternalState.previewPatternColor { buttonColor = previewPatternColor diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift index 1b5d68b720..e9621f1325 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift @@ -14,10 +14,10 @@ import Markdown import GiftItemComponent import StarsAvatarComponent import PasswordSetupUI -import OwnershipTransferController import PresentationDataUtils import AlertComponent import AlertTransferHeaderComponent +import AlertInputFieldComponent public func giftWithdrawAlertController( context: AccountContext, @@ -93,66 +93,97 @@ public func confirmGiftWithdrawalController( present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void ) -> ViewController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let inputState = AlertInputFieldComponent.ExternalState() + + let doneIsEnabled: Signal = inputState.valueSignal + |> map { value in + return !value.isEmpty + } + + let doneInProgressPromise = ValuePromise(false) + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Gift_Withdraw_EnterPassword_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Gift_Withdraw_EnterPassword_Text)) + ) + )) + + var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + placeholder: strings.Channel_OwnershipTransfer_PasswordPlaceholder, + isSecureTextEntry: true, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) + } var dismissImpl: (() -> Void)? - var proceedImpl: (() -> Void)? - - let disposable = MetaDisposable() - - let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.Gift_Withdraw_EnterPassword_Title, text: presentationData.strings.Gift_Withdraw_EnterPassword_Text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?() - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Withdraw_EnterPassword_Done, action: { - proceedImpl?() - })]) - - contentNode.complete = { - proceedImpl?() - } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.theme = presentationData.theme - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - disposable.dispose() - } - dismissImpl = { [weak controller, weak contentNode] in - contentNode?.dismissInput() - controller?.dismissAnimated() - } - proceedImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - contentNode.updateIsChecking(true) - - let signal = context.engine.payments.requestStarGiftWithdrawalUrl(reference: reference, password: contentNode.password) - disposable.set((signal |> deliverOnMainQueue).start(next: { url in + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Gift_Withdraw_EnterPassword_Done, type: .default, action: { + applyImpl?() + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get()) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + doneInProgressPromise.set(true) + + let _ = (context.engine.payments.requestStarGiftWithdrawalUrl(reference: reference, password: inputState.value) + |> deliverOnMainQueue).start(next: { url in dismissImpl?() completion(url) - }, error: { [weak contentNode] error in + }, error: { error in var errorTextAndActions: (String, [TextAlertAction])? switch error { - case .invalidPassword: - contentNode?.animateError() - case .limitExceeded: - errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - default: - errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + case .invalidPassword: + inputState.animateError() + case .limitExceeded: + errorTextAndActions = (strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) + default: + errorTextAndActions = (strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) } - contentNode?.updateIsChecking(false) - + doneInProgressPromise.set(false) + if let (text, actions) = errorTextAndActions { dismissImpl?() present(textAlertController(context: context, title: nil, text: text, actions: actions), nil) } - })) + }) } - - return controller + dismissImpl = { [weak alertController] in + alertController?.dismiss(completion: nil) + } + return alertController } public func giftWithdrawalController( diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD index 9801faf1c3..0afb500308 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD @@ -18,12 +18,12 @@ swift_library( "//submodules/PresentationDataUtils", "//submodules/AccountContext", "//submodules/TextFormat", - "//submodules/AlertUI", "//submodules/PasswordSetupUI", "//submodules/Markdown", "//submodules/ActivityIndicator", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift index cece5ef44f..4ed82f9463 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift @@ -5,461 +5,92 @@ import Display import SwiftSignalKit import TelegramCore import TelegramPresentationData -import ActivityIndicator import TextFormat import AccountContext -import AlertUI import PresentationDataUtils import PasswordSetupUI -import Markdown import OldChannelsController +import ComponentFlow import AlertComponent +import AlertInputFieldComponent -private final class ChannelOwnershipTransferPasswordFieldNode: ASDisplayNode, UITextFieldDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - private let textInputNode: TextFieldNode - private let placeholderNode: ASTextNode - private var clearOnce: Bool = false - private let inputActivityNode: ActivityIndicator - - private var isChecking = false - - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? - - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 22.0, bottom: 15.0, right: 22.0) - private let inputInsets = UIEdgeInsets(top: 5.0, left: 11.0, bottom: 5.0, right: 11.0) - - var password: String { - get { - return self.textInputNode.textField.text ?? "" - } - set { - self.textInputNode.textField.text = newValue - self.placeholderNode.isHidden = !newValue.isEmpty - } - } - - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - init(theme: PresentationTheme, placeholder: String) { - self.theme = theme - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: UIScreenPixel) - - self.textInputNode = TextFieldNode() +private func commitChannelOwnershipTransferController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + peer: EnginePeer, + member: TelegramUser, + present: @escaping (ViewController, Any?) -> Void, + push: @escaping (ViewController) -> Void, + completion: @escaping (EnginePeer.Id?) -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - self.inputActivityNode = ActivityIndicator(type: .custom(theme.list.itemAccentColor, 18.0, 1.5, false)) - - super.init() - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - self.addSubnode(self.inputActivityNode) - - self.inputActivityNode.isHidden = true - } - - override func didLoad() { - super.didLoad() - - self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(14.0), NSAttributedString.Key.foregroundColor: self.theme.actionSheet.inputTextColor] - self.textInputNode.textField.font = Font.regular(14.0) - self.textInputNode.textField.textColor = self.theme.list.itemPrimaryTextColor - self.textInputNode.textField.isSecureTextEntry = true - self.textInputNode.textField.returnKeyType = .done - self.textInputNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.clipsToBounds = true - self.textInputNode.textField.delegate = self - self.textInputNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged) - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textField.tintColor = self.theme.list.itemAccentColor - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: UIScreenPixel) - self.textInputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.textField.textColor = theme.list.itemPrimaryTextColor - self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(14.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor] - self.textInputNode.textField.tintColor = theme.list.itemAccentColor - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(14.0), textColor: theme.actionSheet.inputPlaceholderColor) - } - - func updateIsChecking(_ isChecking: Bool) { - self.isChecking = isChecking - self.inputActivityNode.isHidden = !isChecking - } - - func updateIsInvalid() { - self.clearOnce = true - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight: CGFloat = 30.0 - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) - - let activitySize = CGSize(width: 18.0, height: 18.0) - transition.updateFrame(node: self.inputActivityNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - activitySize.width - 6.0, y: backgroundFrame.minY + floor((backgroundFrame.height - activitySize.height) / 2.0)), size: activitySize)) - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.textChanged?(editableTextNode.textView.text) - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty - } - - @objc func textFieldTextChanged(_ textField: UITextField) { - let text = textField.text ?? "" - self.textChanged?(text) - self.placeholderNode.isHidden = !text.isEmpty - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if self.isChecking { - return false - } - - if string == "\n" { - self.complete?() - return false - } - - if self.clearOnce { - self.clearOnce = false - if range.length > string.count { - textField.text = "" - return false - } - } - - return true - } -} + let inputState = AlertInputFieldComponent.ExternalState() -public final class ChannelOwnershipTransferAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let title: String - private let text: String - - private let titleNode: ASTextNode - private let textNode: ASTextNode - fileprivate let inputFieldNode: ChannelOwnershipTransferPasswordFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - public var complete: (() -> Void)? { - didSet { - self.inputFieldNode.complete = self.complete - } + let doneIsEnabled: Signal = inputState.valueSignal + |> map { value in + return !value.isEmpty } - public var theme: PresentationTheme { - didSet { - self.inputFieldNode.updateTheme(self.theme) - } - } + let doneInProgressPromise = ValuePromise(false) - public override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - public init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, actions: [TextAlertAction]) { - self.strings = strings - self.theme = ptheme - self.title = title - self.text = text - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 4 - - self.inputFieldNode = ChannelOwnershipTransferPasswordFieldNode(theme: ptheme, placeholder: strings.Channel_OwnershipTransfer_PasswordPlaceholder) - - 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.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - self.actionNodes.last?.actionEnabled = false - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.textChanged = { [weak self] text in - if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { - lastNode.actionEnabled = !text.isEmpty - } - } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - public func dismissInput() { - self.inputFieldNode.deactivateInput() - } - - public var password: String { - return self.inputFieldNode.password - } - - public func updateIsChecking(_ checking: Bool) { - self.inputFieldNode.updateIsChecking(checking) - } - - public override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - 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) - } - } - - public override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - - let titleSize = self.titleNode.measure(measureSize) - 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(measureSize) - 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 + 6.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) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + actionsHeight + inputHeight + 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))) + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Channel_OwnershipTransfer_EnterPassword) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Channel_OwnershipTransfer_EnterPasswordText)) + ) + )) + + var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + placeholder: strings.Channel_OwnershipTransfer_PasswordPlaceholder, + isSecureTextEntry: true, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() } - } - 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 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } + ) + ) + )) - public func animateError() { - self.inputFieldNode.updateIsInvalid() - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } -} - -private func commitChannelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, member: TelegramUser, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (EnginePeer.Id?) -> Void) -> ViewController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } var dismissImpl: (() -> Void)? - var proceedImpl: (() -> Void)? - - var pushControllerImpl: ((ViewController) -> Void)? - - let disposable = MetaDisposable() - - let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.Channel_OwnershipTransfer_EnterPassword, text: presentationData.strings.Channel_OwnershipTransfer_EnterPasswordText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?() - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.OwnershipTransfer_Transfer, action: { - proceedImpl?() - })]) - - contentNode.complete = { - proceedImpl?() - } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - disposable.dispose() - } - dismissImpl = { [weak controller, weak contentNode] in - contentNode?.dismissInput() - controller?.dismissAnimated() - } - proceedImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - contentNode.updateIsChecking(true) + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.OwnershipTransfer_Transfer, type: .default, action: { + applyImpl?() + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get()) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + doneInProgressPromise.set(true) let signal: Signal if case let .channel(peer) = peer { - signal = context.peerChannelMemberCategoriesContextsManager.transferOwnership(engine: context.engine, peerId: peer.id, memberId: member.id, password: contentNode.password) |> mapToSignal { _ in + signal = context.peerChannelMemberCategoriesContextsManager.transferOwnership(engine: context.engine, peerId: peer.id, memberId: member.id, password: inputState.value) |> mapToSignal { _ in return .complete() } |> then(.single(nil)) @@ -479,7 +110,7 @@ private func commitChannelOwnershipTransferController(context: AccountContext, u guard let upgradedPeerId = upgradedPeerId else { return .fail(.generic) } - return context.peerChannelMemberCategoriesContextsManager.transferOwnership(engine: context.engine, peerId: upgradedPeerId, memberId: member.id, password: contentNode.password) |> mapToSignal { _ in + return context.peerChannelMemberCategoriesContextsManager.transferOwnership(engine: context.engine, peerId: upgradedPeerId, memberId: member.id, password: inputState.value) |> mapToSignal { _ in return .complete() } |> then(.single(upgradedPeerId)) @@ -488,52 +119,60 @@ private func commitChannelOwnershipTransferController(context: AccountContext, u signal = .never() } - disposable.set((signal |> deliverOnMainQueue).start(next: { upgradedPeerId in + let _ = (signal + |> deliverOnMainQueue).start(next: { upgradedPeerId in dismissImpl?() completion(upgradedPeerId) - }, error: { [weak contentNode] error in + }, error: { error in var isGroup = true if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } + doneInProgressPromise.set(false) + var errorTextAndActions: (String, [TextAlertAction])? switch error { - case .tooMuchJoined: - pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) - return - case .invalidPassword: - contentNode?.animateError() - case .limitExceeded: - errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - case .adminsTooMuch: - errorTextAndActions = (isGroup ? presentationData.strings.Group_OwnershipTransfer_ErrorAdminsTooMuch : presentationData.strings.Channel_OwnershipTransfer_ErrorAdminsTooMuch, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - case .userPublicChannelsTooMuch: - errorTextAndActions = (presentationData.strings.Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - case .userLocatedGroupsTooMuch: - errorTextAndActions = (presentationData.strings.Group_OwnershipTransfer_ErrorLocatedGroupsTooMuch, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - case .userBlocked, .restricted: - errorTextAndActions = (isGroup ? presentationData.strings.Group_OwnershipTransfer_ErrorPrivacyRestricted : presentationData.strings.Channel_OwnershipTransfer_ErrorPrivacyRestricted, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - default: - errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + case .tooMuchJoined: + push(oldChannelsController(context: context, intent: .upgrade)) + return + case .invalidPassword: + inputState.animateError() + case .limitExceeded: + errorTextAndActions = (strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) + case .adminsTooMuch: + errorTextAndActions = (isGroup ? strings.Group_OwnershipTransfer_ErrorAdminsTooMuch : strings.Channel_OwnershipTransfer_ErrorAdminsTooMuch, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) + case .userPublicChannelsTooMuch: + errorTextAndActions = (strings.Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) + case .userLocatedGroupsTooMuch: + errorTextAndActions = (strings.Group_OwnershipTransfer_ErrorLocatedGroupsTooMuch, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) + case .userBlocked, .restricted: + errorTextAndActions = (isGroup ? strings.Group_OwnershipTransfer_ErrorPrivacyRestricted : strings.Channel_OwnershipTransfer_ErrorPrivacyRestricted, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) + default: + errorTextAndActions = (strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) } - contentNode?.updateIsChecking(false) - + if let (text, actions) = errorTextAndActions { dismissImpl?() present(textAlertController(context: context, title: nil, text: text, actions: actions), nil) } - })) + }) } - - pushControllerImpl = { [weak controller] c in - controller?.push(c) + dismissImpl = { [weak alertController] in + alertController?.dismiss(completion: nil) } - - return controller + return alertController } -private func confirmChannelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, member: TelegramUser, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (EnginePeer.Id?) -> Void) -> ViewController { +private func confirmChannelOwnershipTransferController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + peer: EnginePeer, + member: TelegramUser, + present: @escaping (ViewController, Any?) -> Void, + push: @escaping (ViewController) -> Void, + completion: @escaping (EnginePeer.Id?) -> Void +) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } var isGroup = true @@ -558,7 +197,7 @@ private func confirmChannelOwnershipTransferController(context: AccountContext, text: text, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_OwnershipTransfer_ChangeOwner, action: { - present(commitChannelOwnershipTransferController(context: context, peer: peer, member: member, present: present, completion: completion), nil) + present(commitChannelOwnershipTransferController(context: context, peer: peer, member: member, present: present, push: push, completion: completion), nil) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}) ], @@ -567,7 +206,16 @@ private func confirmChannelOwnershipTransferController(context: AccountContext, return controller } -public func channelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, member: TelegramUser, initialError: ChannelOwnershipTransferError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (EnginePeer.Id?) -> Void) -> ViewController { +public func channelOwnershipTransferController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + peer: EnginePeer, + member: TelegramUser, + initialError: ChannelOwnershipTransferError, + present: @escaping (ViewController, Any?) -> Void, + push: @escaping (ViewController) -> Void, + completion: @escaping (EnginePeer.Id?) -> Void +) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings @@ -583,34 +231,34 @@ public func channelOwnershipTransferController(context: AccountContext, updatedP .init(title: strings.Common_OK, type: .default) ] switch initialError { - case .requestPassword: - return confirmChannelOwnershipTransferController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, member: member, present: present, completion: completion) - case .twoStepAuthTooFresh, .authSessionTooFresh: - text = text + strings.OwnershipTransfer_ComeBackLater - case .twoStepAuthMissing: - actions = [ - .init(title: strings.OwnershipTransfer_SetupTwoStepAuth, type: .default, action: { - let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in - if shouldDismiss { - controller.dismiss() - } - }) - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }), - .init(title: strings.Common_Cancel) - ] - case .adminsTooMuch: - title = nil - text = isGroup ? strings.Group_OwnershipTransfer_ErrorAdminsTooMuch : strings.Channel_OwnershipTransfer_ErrorAdminsTooMuch - case .userPublicChannelsTooMuch: - title = nil - text = strings.Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch - case .userBlocked, .restricted: - title = nil - text = isGroup ? strings.Group_OwnershipTransfer_ErrorPrivacyRestricted : strings.Channel_OwnershipTransfer_ErrorPrivacyRestricted - default: - title = nil - text = strings.Login_UnknownError + case .requestPassword: + return confirmChannelOwnershipTransferController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, member: member, present: present, push: push, completion: completion) + case .twoStepAuthTooFresh, .authSessionTooFresh: + text = text + strings.OwnershipTransfer_ComeBackLater + case .twoStepAuthMissing: + actions = [ + .init(title: strings.OwnershipTransfer_SetupTwoStepAuth, type: .default, action: { + let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in + if shouldDismiss { + controller.dismiss() + } + }) + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), + .init(title: strings.Common_Cancel) + ] + case .adminsTooMuch: + title = nil + text = isGroup ? strings.Group_OwnershipTransfer_ErrorAdminsTooMuch : strings.Channel_OwnershipTransfer_ErrorAdminsTooMuch + case .userPublicChannelsTooMuch: + title = nil + text = strings.Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch + case .userBlocked, .restricted: + title = nil + text = isGroup ? strings.Group_OwnershipTransfer_ErrorPrivacyRestricted : strings.Channel_OwnershipTransfer_ErrorPrivacyRestricted + default: + title = nil + text = strings.Login_UnknownError } return AlertScreen( diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift index e94aaa5200..baa29fbf72 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift @@ -8,78 +8,123 @@ import TelegramPresentationData import ActivityIndicator import TextFormat import AccountContext -import AlertUI import PresentationDataUtils import PasswordSetupUI -import Markdown +import ComponentFlow import AlertComponent +import AlertInputFieldComponent -private func commitOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } +private func commitOwnershipTransferController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + present: @escaping (ViewController, Any?) -> Void, + commit: @escaping (String) -> Signal, + completion: @escaping (MessageActionCallbackResult) -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let inputState = AlertInputFieldComponent.ExternalState() + + let doneIsEnabled: Signal = inputState.valueSignal + |> map { value in + return !value.isEmpty + } + + let doneInProgressPromise = ValuePromise(false) + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.OwnershipTransfer_EnterPassword) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.OwnershipTransfer_EnterPasswordText)) + ) + )) + + var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + placeholder: strings.Channel_OwnershipTransfer_PasswordPlaceholder, + isSecureTextEntry: true, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) + } var dismissImpl: (() -> Void)? - var proceedImpl: (() -> Void)? - - let disposable = MetaDisposable() - - let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.OwnershipTransfer_EnterPassword, text: presentationData.strings.OwnershipTransfer_EnterPasswordText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?() - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.OwnershipTransfer_Transfer, action: { - proceedImpl?() - })]) - - contentNode.complete = { - proceedImpl?() - } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.theme = presentationData.theme - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - disposable.dispose() - } - dismissImpl = { [weak controller, weak contentNode] in - contentNode?.dismissInput() - controller?.dismissAnimated() - } - proceedImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - contentNode.updateIsChecking(true) - - disposable.set((commit(contentNode.password) |> deliverOnMainQueue).start(next: { result in - completion(result) + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.OwnershipTransfer_Transfer, type: .default, action: { + applyImpl?() + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get()) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + doneInProgressPromise.set(true) + + let _ = (commit(inputState.value) + |> deliverOnMainQueue).start(next: { result in dismissImpl?() - }, error: { [weak contentNode] error in + completion(result) + }, error: { error in var errorTextAndActions: (String, [TextAlertAction])? switch error { - case .invalidPassword: - contentNode?.animateError() - case .limitExceeded: - errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - case .userBlocked, .restricted: - errorTextAndActions = (presentationData.strings.Group_OwnershipTransfer_ErrorPrivacyRestricted, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - default: - errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + case .invalidPassword: + inputState.animateError() + case .limitExceeded: + errorTextAndActions = (strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) + case .userBlocked, .restricted: + errorTextAndActions = (presentationData.strings.Group_OwnershipTransfer_ErrorPrivacyRestricted, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + default: + errorTextAndActions = (strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) } - contentNode?.updateIsChecking(false) - + doneInProgressPromise.set(false) + if let (text, actions) = errorTextAndActions { dismissImpl?() present(textAlertController(context: context, title: nil, text: text, actions: actions), nil) } - })) + }) } - - return controller + dismissImpl = { [weak alertController] in + alertController?.dismiss(completion: nil) + } + return alertController } -public func ownershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, initialError: MessageActionCallbackError, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { +public func ownershipTransferController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + initialError: MessageActionCallbackError, + present: @escaping (ViewController, Any?) -> Void, + commit: @escaping (String) -> Signal, + completion: @escaping (MessageActionCallbackResult) -> Void +) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD index 496eaecb38..01c08ce49c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD @@ -43,6 +43,7 @@ swift_library( "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift index 4a5015d20f..41b92175b5 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsRevenueWithdrawalController.swift @@ -6,71 +6,102 @@ import TelegramPresentationData import PresentationDataUtils import AccountContext import PasswordSetupUI -import Markdown -import OwnershipTransferController +import ComponentFlow import AlertComponent +import AlertInputFieldComponent public func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, amount: Int64, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let inputState = AlertInputFieldComponent.ExternalState() + + let doneIsEnabled: Signal = inputState.valueSignal + |> map { value in + return !value.isEmpty + } + + let doneInProgressPromise = ValuePromise(false) + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Monetization_Withdraw_EnterPassword_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Monetization_Withdraw_EnterPassword_Text)) + ) + )) + + var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + placeholder: strings.Channel_OwnershipTransfer_PasswordPlaceholder, + isSecureTextEntry: true, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) + } var dismissImpl: (() -> Void)? - var proceedImpl: (() -> Void)? - - let disposable = MetaDisposable() - - let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Title, text: presentationData.strings.Monetization_Withdraw_EnterPassword_Text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?() - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Done, action: { - proceedImpl?() - })]) - - contentNode.complete = { - proceedImpl?() - } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.theme = presentationData.theme - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - disposable.dispose() - } - dismissImpl = { [weak controller, weak contentNode] in - contentNode?.dismissInput() - controller?.dismissAnimated() - } - proceedImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - contentNode.updateIsChecking(true) - - let signal = context.engine.peers.requestStarsRevenueWithdrawalUrl(peerId: peerId, ton: false, amount: amount, password: contentNode.password) - disposable.set((signal |> deliverOnMainQueue).start(next: { url in + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Monetization_Withdraw_EnterPassword_Done, type: .default, action: { + applyImpl?() + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get()) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + doneInProgressPromise.set(true) + + let _ = (context.engine.peers.requestStarsRevenueWithdrawalUrl(peerId: peerId, ton: false, amount: amount, password: inputState.value) + |> deliverOnMainQueue).start(next: { url in dismissImpl?() completion(url) - }, error: { [weak contentNode] error in + }, error: { error in var errorTextAndActions: (String, [TextAlertAction])? switch error { - case .invalidPassword: - contentNode?.animateError() - case .limitExceeded: - errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - default: - errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + case .invalidPassword: + inputState.animateError() + case .limitExceeded: + errorTextAndActions = (strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) + default: + errorTextAndActions = (strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) } - contentNode?.updateIsChecking(false) - + doneInProgressPromise.set(false) + if let (text, actions) = errorTextAndActions { dismissImpl?() present(textAlertController(context: context, title: nil, text: text, actions: actions), nil) } - })) + }) } - - return controller + dismissImpl = { [weak alertController] in + alertController?.dismiss(completion: nil) + } + return alertController } public func starsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index c746dae571..c8dbc216c6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -621,17 +621,15 @@ private final class SheetContent: CombinedComponent { return } if state.currency == .stars { - 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) + if let amount = state.amount, let tonUsdRate = withdrawConfiguration.tonUsdRate, let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate { + var convertedValue = min(convertStarsToTon(amount, tonUsdRate: tonUsdRate, starsUsdRate: usdWithdrawRate), resaleConfiguration.starGiftResaleMaxTonAmount) + if convertedValue < resaleConfiguration.starGiftResaleMinTonAmount { + convertedValue = resaleConfiguration.starGiftResaleMinTonAmount + if case .starGiftResell = component.mode, let controller = controller() as? StarsWithdrawScreen { + controller.presentMinAmountTooltip(convertedValue, 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) + state.amount = StarsAmount(value: convertedValue, nanos: 0) } else { state.amount = StarsAmount(value: 0, nanos: 0) } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index e4fc441032..8b4fee9e55 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -31,7 +31,6 @@ import PeerInfoScreen import PeerInfoStoryGridScreen import ShareWithPeersScreen import ChatEmptyNode -import UndoUI private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData From 974a2f6caa45c2e901748ce6d113d741bd251a13 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Dec 2025 13:34:23 +0400 Subject: [PATCH 6/6] Various improvements --- .../Sources/AttachmentPanel.swift | 3 +- .../Sources/ListMessageSnippetItemNode.swift | 16 ++++-- .../Sources/MediaPickerTitleView.swift | 2 +- .../PremiumUI/Sources/PremiumDemoScreen.swift | 4 +- .../Sources/AttachmentFileController.swift | 2 + .../Sources/EmojiGameStakeScreen.swift | 2 - .../EntityKeyboardBottomPanelComponent.swift | 2 +- .../Sources/GiftSetupScreen.swift | 8 +-- .../Sources/GlassBackgroundComponent.swift | 15 +++++- .../Sources/GlassBarButtonComponent.swift | 8 ++- .../LiquidLens/Sources/LiquidLensView.swift | 14 +++--- .../Sources/ChatHistoryListNode.swift | 50 +++++++++++++------ .../Sources/ContactSelectionController.swift | 2 + .../Sources/SharedAccountContext.swift | 2 + 14 files changed, 91 insertions(+), 39 deletions(-) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index beb821f279..d32f63ef32 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -2076,8 +2076,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } 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 cornerRadius: CGFloat = isSelecting ? min(20.0, panelSize.height * 0.5) : panelSize.height * 0.5 let backgroundOriginX: CGFloat = isSelecting ? panelSideInset : floorToScreenPixels((layout.size.width - panelSize.width) / 2.0) - liquidLensView.update(size: CGSize(width: panelSize.width, height: panelSize.height), selectionOrigin: CGPoint(x: max(0.0, min(panelSize.width - lensSelection.width, lensSelection.x)), y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), inset: 3.0, isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition)) + liquidLensView.update(size: CGSize(width: panelSize.width, height: panelSize.height), cornerRadius: cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(panelSize.width - lensSelection.width, lensSelection.x)), y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), inset: 3.0, isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition)) transition.updatePosition(layer: liquidLensView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5)) transition.updateBounds(layer: liquidLensView.layer, bounds: CGRect(origin: .zero, size: panelSize)) diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index de53829137..fd7134aa1c 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -18,9 +18,17 @@ import TelegramStringFormatting import WallpaperResources import UrlEscaping -private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold) +private let iconFont = Font.with(size: 28.0, design: .round, weight: .bold) private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500)) +private let iconTextGlassBackgroundImage = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(rgb: 0xFF9500).cgColor) + + let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 12.0) + context.addPath(path.cgPath) + context.fillPath() +}) public final class ListMessageSnippetItemNode: ListMessageNode { private let contextSourceNode: ContextExtractedContentContainingNode @@ -273,7 +281,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { var iconImageReferenceAndRepresentation: (AnyMediaReference, TelegramMediaImageRepresentation)? var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - let applyIconTextBackgroundImage = iconTextBackgroundImage + let applyIconTextBackgroundImage = item.systemStyle == .glass ? iconTextGlassBackgroundImage : iconTextBackgroundImage var primaryUrl: String? @@ -637,7 +645,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { var iconImageApply: (() -> Void)? if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { let iconSize = CGSize(width: 40.0, height: 40.0) - let imageCorners = ImageCorners(radius: 6.0) + let imageCorners = ImageCorners(radius: item.systemStyle == .glass ? 12.0 : 6.0, curve: item.systemStyle == .glass ? .continuous : .circular) let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor) iconImageApply = iconImageLayout(arguments) } @@ -754,7 +762,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { } let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 12.0), size: CGSize(width: 40.0, height: 40.0)) - transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floorToScreenPixels((iconFrame.height - iconTextLayout.size.height) / 2.0) + 2.0), size: iconTextLayout.size)) + transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - iconTextLayout.size.width) / 2.0) + 1.0 - UIScreenPixel, y: iconFrame.minY + floorToScreenPixels((iconFrame.height - iconTextLayout.size.height) / 2.0) + 2.0), size: iconTextLayout.size)) let _ = iconTextApply() diff --git a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift index 49cf142fb7..cbb8991221 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift @@ -260,7 +260,7 @@ final class MediaPickerTitleView: UIView { totalHeight += subtitleSize.height } - let verticalOffset: CGFloat = self.glass ? 3.0 : 0.0 + let verticalOffset: CGFloat = self.glass ? -1.0 : 0.0 let arrowOffset: CGFloat = self.glass ? 1.0 : 5.0 var totalWidth = titleSize.width diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index e531a8b126..2decd5b60c 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1133,7 +1133,7 @@ private final class DemoSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), + size: CGSize(width: 44.0, height: 44.0), backgroundColor: UIColor(rgb: 0x7f76f4), isDark: false, state: .tintedGlass, @@ -1147,7 +1147,7 @@ private final class DemoSheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift index 9d5bc66c52..3a3a979c70 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift @@ -465,6 +465,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre backgroundColor: nil, isDark: presentationData.theme.overallDarkAppearance, state: .generic, + animateScale: false, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -492,6 +493,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre backgroundColor: nil, isDark: presentationData.theme.overallDarkAppearance, state: .generic, + animateScale: false, component: AnyComponentWithIdentity(id: "search", component: AnyComponent( BundleIconComponent( name: "Navigation/Search", diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift index 78a6b22f96..20c8c97b82 100644 --- a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift @@ -319,8 +319,6 @@ private final class SheetContent: CombinedComponent { id: AnyHashable(0), component: AnyComponent(HStack(buttonItems, spacing: 7.0)) ), - isEnabled: true, - displaysProgress: false, action: { [weak state] in if let state, let amount = state.amount, let controller = controller() as? EmojiGameStakeScreen { controller.complete(amount: amount) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index ae2cebc6c4..9a4809292c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -514,7 +514,7 @@ final class EntityKeyboardBottomPanelComponent: Component { lensSelection = (selectionGestureState.currentX, lensSelection.width) } - transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabsSize.width) / 2.0), y: 3.0), size: tabsSize)) + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabsSize.width) / 2.0), y: 0.0), size: tabsSize)) self.liquidLensView.update(size: CGSize(width: tabsSize.width, height: tabsSize.height), selectionOrigin: CGPoint(x: max(0.0, min(tabsSize.width - lensSelection.width, lensSelection.x)), y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: tabsSize.height), inset: 3.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: activeContentId == nil, transition: transition) var removedIconViewIds: [AnyHashable] = [] diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index ecc84a4338..632e81c409 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -1117,10 +1117,10 @@ private final class GiftSetupScreenComponent: Component { let closeButtonSize = self.closeButton.update( transition: .immediate, component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -1135,7 +1135,7 @@ private final class GiftSetupScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) let closeButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + 16.0, y: 16.0), size: closeButtonSize) if let closeButtonView = self.closeButton.view { diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index 366cfeb133..e9230420bd 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -998,12 +998,20 @@ public final class GlassBackgroundComponent: Component { private let cornerRadius: CGFloat private let isDark: Bool private let tintColor: GlassBackgroundView.TintColor + private let isInteractive: Bool - public init(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: GlassBackgroundView.TintColor) { + public init( + size: CGSize, + cornerRadius: CGFloat, + isDark: Bool, + tintColor: GlassBackgroundView.TintColor, + isInteractive: Bool = false + ) { self.size = size self.cornerRadius = cornerRadius self.isDark = isDark self.tintColor = tintColor + self.isInteractive = isInteractive } public static func == (lhs: GlassBackgroundComponent, rhs: GlassBackgroundComponent) -> Bool { @@ -1019,12 +1027,15 @@ public final class GlassBackgroundComponent: Component { if lhs.tintColor != rhs.tintColor { return false } + if lhs.isInteractive != rhs.isInteractive { + return false + } return true } public final class View: GlassBackgroundView { func update(component: GlassBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.update(size: component.size, cornerRadius: component.cornerRadius, isDark: component.isDark, tintColor: component.tintColor, transition: transition) + self.update(size: component.size, cornerRadius: component.cornerRadius, isDark: component.isDark, tintColor: component.tintColor, isInteractive: component.isInteractive, transition: transition) return component.size } diff --git a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift index 4ed935590f..55f1660ed7 100644 --- a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift +++ b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift @@ -17,6 +17,7 @@ public final class GlassBarButtonComponent: Component { public let isDark: Bool public let state: DisplayState? public let isEnabled: Bool + public let animateScale: Bool public let component: AnyComponentWithIdentity public let action: ((UIView) -> Void)? @@ -26,6 +27,7 @@ public final class GlassBarButtonComponent: Component { isDark: Bool, state: DisplayState? = nil, isEnabled: Bool = true, + animateScale: Bool = true, component: AnyComponentWithIdentity, action: ((UIView) -> Void)? ) { @@ -34,6 +36,7 @@ public final class GlassBarButtonComponent: Component { self.isDark = isDark self.state = state self.isEnabled = isEnabled + self.animateScale = animateScale self.component = component self.action = action } @@ -54,6 +57,9 @@ public final class GlassBarButtonComponent: Component { if lhs.isEnabled != rhs.isEnabled { return false } + if lhs.animateScale != rhs.animateScale { + return false + } if lhs.component != rhs.component { return false } @@ -88,7 +94,7 @@ public final class GlassBarButtonComponent: Component { self.containerView.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.containerView.highligthedChanged = { [weak self] highlighted in - guard let self else { + guard let self, let component = self.component, component.animateScale else { return } if highlighted { diff --git a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift index 7f327300a6..7b9378cf00 100644 --- a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift +++ b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift @@ -66,6 +66,7 @@ public final class LiquidLensView: UIView { private struct Params: Equatable { var size: CGSize + var cornerRadius: CGFloat? var selectionOrigin: CGPoint var selectionSize: CGSize var inset: CGFloat @@ -73,8 +74,9 @@ public final class LiquidLensView: UIView { var isLifted: Bool var isCollapsed: Bool - init(size: CGSize, selectionOrigin: CGPoint, selectionSize: CGSize, inset: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { + init(size: CGSize, cornerRadius: CGFloat?, selectionOrigin: CGPoint, selectionSize: CGSize, inset: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { self.size = size + self.cornerRadius = cornerRadius self.selectionOrigin = selectionOrigin self.selectionSize = selectionSize self.inset = inset @@ -286,8 +288,8 @@ public final class LiquidLensView: UIView { lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: view) } - public func update(size: CGSize, selectionOrigin: CGPoint, selectionSize: CGSize, inset: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { - let params = Params(size: size, selectionOrigin: selectionOrigin, selectionSize: selectionSize, inset: inset, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) + public func update(size: CGSize, cornerRadius: CGFloat? = nil, selectionOrigin: CGPoint, selectionSize: CGSize, inset: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { + let params = Params(size: size, cornerRadius: cornerRadius, selectionOrigin: selectionOrigin, selectionSize: selectionSize, inset: inset, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) if self.params == params { return } @@ -421,7 +423,7 @@ public final class LiquidLensView: UIView { if let backgroundView = self.backgroundView { transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) - backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.isDark, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.isDark ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) + backgroundView.update(size: params.size, cornerRadius: params.cornerRadius ?? (params.size.height * 0.5), isDark: params.isDark, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.isDark ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) } if self.contentView.bounds.size != params.size { @@ -432,7 +434,7 @@ public final class LiquidLensView: UIView { } self.contentView.clipsToBounds = false }) - transition.setCornerRadius(layer: self.contentView.layer, cornerRadius: params.size.height * 0.5) + transition.setCornerRadius(layer: self.contentView.layer, cornerRadius: params.cornerRadius ?? (params.size.height * 0.5)) self.liftedContainerView.clipsToBounds = true transition.setFrame(view: self.liftedContainerView, frame: CGRect(origin: CGPoint(), size: params.size), completion: { [weak self] completed in @@ -441,7 +443,7 @@ public final class LiquidLensView: UIView { } self.liftedContainerView.clipsToBounds = false }) - transition.setCornerRadius(layer: self.liftedContainerView.layer, cornerRadius: params.size.height * 0.5) + transition.setCornerRadius(layer: self.liftedContainerView.layer, cornerRadius: params.cornerRadius ?? (params.size.height * 0.5)) } let baseLensFrame = CGRect(origin: CGPoint(x: params.selectionOrigin.x, y: 0.0), size: CGSize(width: params.selectionSize.width, height: params.size.height)) diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index baccfdfddc..28b0e5211c 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -13,6 +13,7 @@ import AccountContext import TemporaryCachedPeerDataManager import Emoji import AppBundle +import ItemListUI import ListMessageItem import AccountContext import ChatInterfaceState @@ -215,7 +216,7 @@ extension ListMessageItemInteraction { } } -private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, isSavedMusic: Bool, canReorder: Bool, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, isSavedMusic: Bool, canReorder: Bool, entries: [ChatHistoryViewTransitionInsertEntry], systemStyle: ItemListSystemStyle) -> [ListViewInsertItem] { var disableFloatingDateHeaders = false if case .customChatContents = chatLocation { disableFloatingDateHeaders = true @@ -238,7 +239,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca case .allButLast: displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId } - item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, isSavedMusic: isSavedMusic, canReorder: canReorder) + item = ListMessageItem(presentationData: presentationData, systemStyle: systemStyle, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, isSavedMusic: isSavedMusic, canReorder: canReorder) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): @@ -248,7 +249,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders) case .list: assertionFailure() - item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) + item = ListMessageItem(presentationData: presentationData, systemStyle: systemStyle, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .UnreadEntry(_, presentationData): @@ -270,7 +271,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca } } -private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, isSavedMusic: Bool, canReorder: Bool, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, isSavedMusic: Bool, canReorder: Bool, entries: [ChatHistoryViewTransitionUpdateEntry], systemStyle: ItemListSystemStyle) -> [ListViewUpdateItem] { var disableFloatingDateHeaders = false if case .customChatContents = chatLocation { disableFloatingDateHeaders = true @@ -293,7 +294,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca case .allButLast: displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId } - item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, isSavedMusic: isSavedMusic, canReorder: canReorder) + item = ListMessageItem(presentationData: presentationData, systemStyle: systemStyle, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, isSavedMusic: isSavedMusic, canReorder: canReorder) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): @@ -303,7 +304,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders) case .list: assertionFailure() - item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) + item = ListMessageItem(presentationData: presentationData, systemStyle: systemStyle, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .UnreadEntry(_, presentationData): @@ -325,8 +326,8 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca } } -private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, isSavedMusic: Bool, canReorder: Bool, animateFromPreviousFilter: Bool, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: canReorder, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: canReorder, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators, animateFromPreviousFilter: animateFromPreviousFilter) +private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, isSavedMusic: Bool, canReorder: Bool, animateFromPreviousFilter: Bool, transition: ChatHistoryViewTransition, systemStyle: ItemListSystemStyle) -> ChatHistoryListViewTransition { + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: canReorder, entries: transition.insertEntries, systemStyle: systemStyle), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: canReorder, entries: transition.updateEntries, systemStyle: systemStyle), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators, animateFromPreviousFilter: animateFromPreviousFilter) } final class ChatHistoryTransactionOpaqueState { @@ -466,6 +467,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto static let fixedAdMessageStableId: UInt32 = UInt32.max - 5000 public let context: AccountContext + private let systemStyle: ItemListSystemStyle private(set) var chatLocation: ChatLocation private let chatLocationContextHolder: Atomic private let source: ChatHistoryListSource @@ -753,7 +755,23 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private let initTimestamp: Double - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), chatLocation: ChatLocation, chatLocationContextHolder: Atomic, adMessagesContext: AdMessagesHistoryContext?, tag: HistoryViewInputTag?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles, rotated: Bool = false, isChatPreview: Bool, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) { + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal), + systemStyle: ItemListSystemStyle = .legacy, + chatLocation: ChatLocation, + chatLocationContextHolder: Atomic, + adMessagesContext: AdMessagesHistoryContext?, + tag: HistoryViewInputTag?, + source: ChatHistoryListSource, + subject: ChatControllerSubject?, + controllerInteraction: ChatControllerInteraction, + selectedMessages: Signal?, NoError>, + mode: ChatHistoryListMode = .bubbles, + rotated: Bool = false, + isChatPreview: Bool, + messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl? + ) { self.initTimestamp = CFAbsoluteTimeGetCurrent() var tag = tag @@ -762,6 +780,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } self.context = context + self.systemStyle = systemStyle self.chatLocation = chatLocation self.chatLocationContextHolder = chatLocationContextHolder self.source = source @@ -1312,6 +1331,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let messageTransitionNode = self.messageTransitionNode let mode = self.mode let rotated = self.rotated + let systemStyle = self.systemStyle var resetScrollingMessageId: (index: MessageIndex, offset: CGFloat)? @@ -1792,7 +1812,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto return historyViewUpdateValue } } - + let startTime = CFAbsoluteTimeGetCurrent() var measure_isFirstTime = true let messageViewQueue = Queue.mainQueue() @@ -1922,7 +1942,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let forceSynchronous = true let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, chatLocation: chatLocation, source: source, controllerInteraction: controllerInteraction, scrollPosition: nil, scrollAnimationCurve: nil, initialData: initialData?.initialData, keyboardButtonsMessage: nil, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: false, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: false) - var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: previousViewValue.associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: 0, isSavedMusic: isSavedMusic, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) + var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: previousViewValue.associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: 0, isSavedMusic: isSavedMusic, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition, systemStyle: systemStyle) if disableAnimations { mappedTransition.options.remove(.AnimateInsertion) @@ -2309,7 +2329,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, source: source, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: !isSavedMusic || forceUpdateAll) - var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: processedView.filteredEntries.count > 1 && canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) + var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: processedView.filteredEntries.count > 1 && canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition, systemStyle: systemStyle) if disableAnimations { mappedTransition.options.remove(.AnimateInsertion) @@ -4548,7 +4568,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto case .allButLast: displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId } - item = ListMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch) + item = ListMessageItem(presentationData: presentationData, systemStyle: self.systemStyle, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch) } let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil) @@ -4569,7 +4589,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders) case .list: assertionFailure() - item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) + item = ListMessageItem(presentationData: presentationData, systemStyle: self.systemStyle, context: self.context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) } let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil) @@ -4626,7 +4646,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto case .allButLast: displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId } - item = ListMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch) + item = ListMessageItem(presentationData: presentationData, systemStyle: self.systemStyle, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch) } let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil) self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 9da3206638..22a2c985e5 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -246,6 +246,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController backgroundColor: nil, isDark: self.presentationData.theme.overallDarkAppearance, state: .generic, + animateScale: false, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -267,6 +268,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController backgroundColor: nil, isDark: self.presentationData.theme.overallDarkAppearance, state: .generic, + animateScale: false, component: AnyComponentWithIdentity(id: "search", component: AnyComponent( BundleIconComponent( name: "Navigation/Search", diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b6de849147..c233cf1b38 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -10,6 +10,7 @@ import TelegramCallsUI import TelegramUIPreferences import AccountContext import DeviceLocationManager +import ItemListUI import LegacyUI import ChatListUI import PeersNearbyUI @@ -2244,6 +2245,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return ChatHistoryListNodeImpl( context: context, updatedPresentationData: updatedPresentationData, + systemStyle: .glass, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, adMessagesContext: nil,