Various improvements

This commit is contained in:
Ilya Laktyushin 2025-12-25 01:00:37 +04:00
parent 57cdc60862
commit 0f53130216
21 changed files with 1150 additions and 1441 deletions

View file

@ -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)
}))
})

View file

@ -22,8 +22,7 @@ public func webBrowserDomainController(context: AccountContext, updatedPresentat
return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
let doneInProgressValuePromise = ValuePromise<Bool>(false)
let doneInProgress = doneInProgressValuePromise.get()
let doneInProgressPromise = ValuePromise<Bool>(false)
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
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()

View file

@ -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",

View file

@ -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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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<Bool, NoError> = inputState.valueSignal
|> map { value in
return !value.isEmpty
}
let doneInProgressPromise = ValuePromise<Bool>(false)
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
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<PresentationData, NoError>)
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
}

View file

@ -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 {

View file

@ -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,

View file

@ -38,14 +38,14 @@ private final class AlertScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let configuration: AlertScreen.Configuration
let content: [AnyComponentWithIdentity<AlertComponentEnvironment>]
let actions: [AlertScreen.Action]
let content: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>
let actions: Signal<[AlertScreen.Action], NoError>
let ready: Promise<Bool>
init(
configuration: AlertScreen.Configuration,
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
actions: [AlertScreen.Action],
content: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
actions: Signal<[AlertScreen.Action], NoError>,
ready: Promise<Bool>
) {
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<AlertComponentEnvironment>] = [:]
private var actions: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
private var disposable: Disposable?
private var content: [AnyComponentWithIdentity<AlertComponentEnvironment>]?
private var actions: [AlertScreen.Action]?
private var contentItems: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
private var actionItems: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
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<AnyHashable> = Set()
for content in component.content {
if contentOriginY.isZero {
contentOriginY += contentTopInset
} else {
contentOriginY += contentSpacing
}
validContentIds.insert(content.id)
let item: ComponentView<AlertComponentEnvironment>
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<AlertComponentEnvironment>
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<AnyHashable> = Set()
for action in component.actions {
validActionIds.insert(action.id)
let item: ComponentView<AlertComponentEnvironment>
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<AnyHashable> = Set()
for action in actions {
validActionIds.insert(action.id)
let item: ComponentView<AlertComponentEnvironment>
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<Bool, NoError>
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<AlertComponentEnvironment>],
actions: [Action],
contentSignal: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
actionsSignal: Signal<[Action], NoError>,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
) {
let componentReady = Promise<Bool>()
@ -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<AlertComponentEnvironment>],
actions: [Action],
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
) {
self.init(
configuration: configuration,
contentSignal: .single(content),
actionsSignal: .single(actions),
updatedPresentationData: updatedPresentationData
)
}
public convenience init(
context: AccountContext,
configuration: Configuration = Configuration(),

View file

@ -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)
}
}

View file

@ -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<Empty>()
private var didAppear = false
public init(
context: AccountContext?,
presentationData: PresentationData,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
configuration: Configuration = AlertScreen.Configuration(),
contentSignal: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
actionsSignal: Signal<[AlertScreen.Action], NoError>,
navigationController: NavigationController?,
chatPeerId: EnginePeer.Id,
showBalance: Bool = true,
currencySignal: Signal<CurrencyAmount.Currency, NoError> = .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<PresentationData, NoError>)
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<PresentationData, NoError>)? = 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<PresentationData, NoError>)
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)

View file

@ -73,23 +73,45 @@ public func factCheckAlertController(
)
))
let doneIsRemove: Signal<Bool, NoError>
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<PresentationData, NoError>)
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

View file

@ -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",

View file

@ -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<Empty>()
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let giftView = ComponentView<Empty>()
private let arrow = ComponentView<Empty>()
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<CurrencyAmount.Currency>(.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<AlertComponentEnvironment>] = []
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<Empty>()
private var component: AlertCurrencyComponent?
private weak var state: EmptyComponentState?
func update(component: AlertCurrencyComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, 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<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -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

View file

@ -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<Bool, NoError> = inputState.valueSignal
|> map { value in
return !value.isEmpty
}
let doneInProgressPromise = ValuePromise<Bool>(false)
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
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<PresentationData, NoError>)
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(

View file

@ -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",

View file

@ -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<PresentationData, NoError>)? = 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<Bool, NoError> = inputState.valueSignal
|> map { value in
return !value.isEmpty
}
public var theme: PresentationTheme {
didSet {
self.inputFieldNode.updateTheme(self.theme)
}
}
let doneInProgressPromise = ValuePromise<Bool>(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<AlertComponentEnvironment>] = []
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<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
}
}
private func commitChannelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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<EnginePeer.Id?, ChannelOwnershipTransferError>
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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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(

View file

@ -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<PresentationData, NoError>)? = nil, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal<MessageActionCallbackResult, MessageActionCallbackError>, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
private func commitOwnershipTransferController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
present: @escaping (ViewController, Any?) -> Void,
commit: @escaping (String) -> Signal<MessageActionCallbackResult, MessageActionCallbackError>,
completion: @escaping (MessageActionCallbackResult) -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let inputState = AlertInputFieldComponent.ExternalState()
let doneIsEnabled: Signal<Bool, NoError> = inputState.valueSignal
|> map { value in
return !value.isEmpty
}
let doneInProgressPromise = ValuePromise<Bool>(false)
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
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<PresentationData, NoError>)
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<PresentationData, NoError>)? = nil, initialError: MessageActionCallbackError, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal<MessageActionCallbackResult, MessageActionCallbackError>, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController {
public func ownershipTransferController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
initialError: MessageActionCallbackError,
present: @escaping (ViewController, Any?) -> Void,
commit: @escaping (String) -> Signal<MessageActionCallbackResult, MessageActionCallbackError>,
completion: @escaping (MessageActionCallbackResult) -> Void
) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings

View file

@ -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",

View file

@ -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<PresentationData, NoError>)? = 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<Bool, NoError> = inputState.valueSignal
|> map { value in
return !value.isEmpty
}
let doneInProgressPromise = ValuePromise<Bool>(false)
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
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<PresentationData, NoError>)
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<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {

View file

@ -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)
}

View file

@ -31,7 +31,6 @@ import PeerInfoScreen
import PeerInfoStoryGridScreen
import ShareWithPeersScreen
import ChatEmptyNode
import UndoUI
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData