mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Various improvements
This commit is contained in:
parent
57cdc60862
commit
0f53130216
21 changed files with 1150 additions and 1441 deletions
|
|
@ -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)
|
||||
}))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import PeerInfoScreen
|
|||
import PeerInfoStoryGridScreen
|
||||
import ShareWithPeersScreen
|
||||
import ChatEmptyNode
|
||||
import UndoUI
|
||||
|
||||
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
||||
private var presentationData: PresentationData
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue