From 0f531302162b8e34e30c14ced51d9a7baf866f2f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 25 Dec 2025 01:00:37 +0400 Subject: [PATCH] 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