mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-03-30 18:49:07 +00:00
1043 lines
51 KiB
Swift
1043 lines
51 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import AccountContext
|
|
import TelegramPresentationData
|
|
import ComponentFlow
|
|
import ViewControllerComponent
|
|
import SheetComponent
|
|
import MultilineTextComponent
|
|
import GlassBarButtonComponent
|
|
import ButtonComponent
|
|
import PlainButtonComponent
|
|
import PresentationDataUtils
|
|
import BundleIconComponent
|
|
import ListSectionComponent
|
|
import ListActionItemComponent
|
|
import AvatarComponent
|
|
import Markdown
|
|
import PhoneNumberFormat
|
|
import ContextUI
|
|
import AccountUtils
|
|
import GlassBackgroundComponent
|
|
import AccountPeerContextItem
|
|
import ActivityIndicator
|
|
import LottieComponent
|
|
import LottieComponentResourceContent
|
|
|
|
private final class AuthConfirmationSheetContent: CombinedComponent {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let requestSubject: MessageActionUrlSubject
|
|
let subject: MessageActionUrlAuthResult
|
|
let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
|
|
let cancel: (Bool) -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
requestSubject: MessageActionUrlSubject,
|
|
subject: MessageActionUrlAuthResult,
|
|
completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void,
|
|
cancel: @escaping (Bool) -> Void
|
|
) {
|
|
self.context = context
|
|
self.requestSubject = requestSubject
|
|
self.subject = subject
|
|
self.completion = completion
|
|
self.cancel = cancel
|
|
}
|
|
|
|
static func ==(lhs: AuthConfirmationSheetContent, rhs: AuthConfirmationSheetContent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class State: ComponentState {
|
|
private let context: AccountContext
|
|
private let requestSubject: MessageActionUrlSubject
|
|
private let subject: MessageActionUrlAuthResult
|
|
private let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
|
|
|
|
private let disposables = DisposableSet()
|
|
|
|
var peer: EnginePeer?
|
|
var forcedAccount: (AccountContext, EnginePeer)?
|
|
|
|
fileprivate var inProgress = false
|
|
var allowWrite = true
|
|
weak var controller: ViewController?
|
|
|
|
var displayEmoji = false
|
|
var matchCodes: [String]?
|
|
var selectedMatchCode: String?
|
|
|
|
init(context: AccountContext, requestSubject: MessageActionUrlSubject, subject: MessageActionUrlAuthResult, completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void) {
|
|
self.context = context
|
|
self.requestSubject = requestSubject
|
|
self.subject = subject
|
|
self.completion = completion
|
|
|
|
super.init()
|
|
|
|
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
|
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
|
guard let self, let peer else {
|
|
return
|
|
}
|
|
self.peer = peer
|
|
self.updated()
|
|
})
|
|
|
|
if case let .request(_, _, _, flags, matchCodes, _) = self.subject, let matchCodes {
|
|
if flags.contains(.showMatchCodesFirst) {
|
|
self.displayEmoji = true
|
|
self.matchCodes = matchCodes.shuffled()
|
|
} else {
|
|
for code in matchCodes {
|
|
var file: TelegramMediaFile?
|
|
if let item = context.animatedEmojiStickersValue[code] {
|
|
file = item.first?.file._parse()
|
|
} else if let item = context.animatedEmojiStickersValue[code.strippedEmoji] {
|
|
file = item.first?.file._parse()
|
|
}
|
|
if let file {
|
|
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if case let .request(_, _, _, _, _, userIdHint) = self.subject, let userIdHint, userIdHint != context.account.peerId {
|
|
let _ = (activeAccountsAndPeers(context: self.context, includePrimary: true)
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] primary, other in
|
|
guard let self else {
|
|
return
|
|
}
|
|
for (accountContext, peer, _) in other {
|
|
if peer.id == userIdHint {
|
|
self.forcedAccount = (accountContext, peer)
|
|
self.updated()
|
|
|
|
accountContext.account.shouldBeServiceTaskMaster.set(.single(.now))
|
|
let _ = accountContext.engine.messages.requestMessageActionUrlAuth(subject: requestSubject).start()
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.disposables.dispose()
|
|
|
|
if !self.inProgress {
|
|
if let (context, _) = self.forcedAccount {
|
|
context.account.shouldBeServiceTaskMaster.set(.single(.never))
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkMatchCode(_ matchCode: String) {
|
|
guard case let .url(url, _) = self.requestSubject else {
|
|
return
|
|
}
|
|
self.selectedMatchCode = matchCode
|
|
self.updated(transition: .easeInOut(duration: 0.2))
|
|
|
|
let _ = (self.context.engine.messages.checkUrlAuthMatchCode(url: url, matchCode: matchCode)
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if result {
|
|
self.displayEmoji = false
|
|
self.updated(transition: .spring(duration: 0.4))
|
|
} else {
|
|
let accountContext = self.forcedAccount?.0 ?? self.context
|
|
guard let accountPeer = self.forcedAccount?.1 ?? self.peer else {
|
|
return
|
|
}
|
|
|
|
self.completion(accountContext, accountPeer, .failed)
|
|
}
|
|
})
|
|
}
|
|
|
|
func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) {
|
|
guard case let .request(domain, _, _, _, _, _) = self.subject else {
|
|
return
|
|
}
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
|
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
|
guard let self, case let .user(user) = peer, let phone = user.phone else {
|
|
return
|
|
}
|
|
let phoneNumber = formatPhoneNumber(context: self.context, number: phone).replacingOccurrences(of: " ", with: "\u{00A0}")
|
|
|
|
let alertController = textAlertController(
|
|
context: self.context,
|
|
title: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Title,
|
|
text: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Text(domain, phoneNumber).string,
|
|
actions: [
|
|
TextAlertAction(type: .genericAction, title: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Deny, action: {
|
|
commit(false)
|
|
}),
|
|
TextAlertAction(type: .defaultAction, title: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Allow, action: {
|
|
commit(true)
|
|
})
|
|
]
|
|
)
|
|
self.controller?.present(alertController, in: .window(.root))
|
|
})
|
|
}
|
|
|
|
func presentAccountSwitchMenu(sourceView: GlassContextExtractableContainer) {
|
|
guard let controller = self.controller else {
|
|
return
|
|
}
|
|
|
|
let context = self.context
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let items: Signal<[ContextMenuItem], NoError> = activeAccountsAndPeers(context: self.context, includePrimary: true)
|
|
|> take(1)
|
|
|> map { primary, other -> [ContextMenuItem] in
|
|
var items: [ContextMenuItem] = []
|
|
var existingIds = Set<EnginePeer.Id>()
|
|
if let (_, peer) = primary {
|
|
items.append(.custom(AccountPeerContextItem(context: context, account: context.account, peer: peer, action: { [weak self] _, f in
|
|
f(.default)
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let (context, _) = self.forcedAccount {
|
|
context.account.shouldBeServiceTaskMaster.set(.single(.never))
|
|
}
|
|
|
|
self.forcedAccount = nil
|
|
self.updated()
|
|
}), true))
|
|
existingIds.insert(peer.id)
|
|
}
|
|
|
|
for (accountContext, peer, _) in other {
|
|
guard !existingIds.contains(peer.id) else {
|
|
continue
|
|
}
|
|
items.append(.custom(AccountPeerContextItem(context: accountContext, account: accountContext.account, peer: peer, action: { [weak self] _, f in
|
|
f(.default)
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let (context, _) = self.forcedAccount {
|
|
context.account.shouldBeServiceTaskMaster.set(.single(.never))
|
|
}
|
|
|
|
accountContext.account.shouldBeServiceTaskMaster.set(.single(.now))
|
|
|
|
self.forcedAccount = (accountContext, peer)
|
|
self.updated()
|
|
}), true))
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
let contextController = makeContextController(presentationData: presentationData, source: .reference(AuthConfirmationReferenceContentSource(controller: controller, sourceView: sourceView)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: nil)
|
|
controller.presentInGlobalOverlay(contextController)
|
|
}
|
|
}
|
|
|
|
func makeState() -> State {
|
|
return State(context: self.context, requestSubject: self.requestSubject, subject: self.subject, completion: self.completion)
|
|
}
|
|
|
|
static var body: Body {
|
|
let closeButton = Child(GlassBarButtonComponent.self)
|
|
let accountButton = Child(AccountSwitchComponent.self)
|
|
let avatar = Child(AvatarComponent.self)
|
|
let title = Child(MultilineTextComponent.self)
|
|
let description = Child(MultilineTextComponent.self)
|
|
|
|
let emojiTitle = Child(MultilineTextComponent.self)
|
|
let emojiDescription = Child(MultilineTextComponent.self)
|
|
let emojis = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
|
|
|
let clientSection = Child(ListSectionComponent.self)
|
|
let optionsSection = Child(ListSectionComponent.self)
|
|
let cancelButton = Child(ButtonComponent.self)
|
|
let doneButton = Child(ButtonComponent.self)
|
|
|
|
return { context in
|
|
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
|
let component = context.component
|
|
let theme = environment.theme
|
|
let strings = environment.strings
|
|
let state = context.state
|
|
if state.controller == nil {
|
|
state.controller = environment.controller()
|
|
}
|
|
|
|
let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
guard case let .request(domain, bot, clientData, flags, matchCodes, _) = component.subject else {
|
|
fatalError()
|
|
}
|
|
|
|
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
|
|
|
let closeButton = closeButton.update(
|
|
component: GlassBarButtonComponent(
|
|
size: CGSize(width: 44.0, height: 44.0),
|
|
backgroundColor: nil,
|
|
isDark: theme.overallDarkAppearance,
|
|
state: .glass,
|
|
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
|
BundleIconComponent(
|
|
name: "Navigation/Close",
|
|
tintColor: theme.chat.inputPanel.panelControlColor
|
|
)
|
|
)),
|
|
action: { _ in
|
|
component.cancel(true)
|
|
}
|
|
),
|
|
availableSize: CGSize(width: 44.0, height: 44.0),
|
|
transition: .immediate
|
|
)
|
|
context.add(closeButton
|
|
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
|
|
)
|
|
|
|
if let peer = state.peer {
|
|
let accountButton = accountButton.update(
|
|
component: AccountSwitchComponent(
|
|
context: state.forcedAccount?.0 ?? component.context,
|
|
theme: environment.theme,
|
|
peer: state.forcedAccount?.1 ?? peer,
|
|
canSwitch: true,
|
|
isVisible: true,
|
|
action: { [weak state] sourceView in
|
|
state?.presentAccountSwitchMenu(sourceView: sourceView)
|
|
}
|
|
),
|
|
availableSize: context.availableSize,
|
|
transition: .immediate
|
|
)
|
|
context.add(accountButton
|
|
.position(CGPoint(x: context.availableSize.width - 16.0 - accountButton.size.width / 2.0, y: 16.0 + accountButton.size.height / 2.0))
|
|
)
|
|
}
|
|
|
|
let titleFont = Font.bold(24.0)
|
|
let textFont = Font.regular(15.0)
|
|
let boldTextFont = Font.semibold(15.0)
|
|
|
|
let complete: (String?) -> Void = { [weak state] matchCode in
|
|
guard let state else {
|
|
return
|
|
}
|
|
var allowWrite = false
|
|
if flags.contains(.requestWriteAccess) && state.allowWrite {
|
|
allowWrite = true
|
|
}
|
|
|
|
let accountContext = state.forcedAccount?.0 ?? component.context
|
|
guard let accountPeer = state.forcedAccount?.1 ?? state.peer else {
|
|
return
|
|
}
|
|
|
|
if flags.contains(.requestPhoneNumber) {
|
|
state.displayPhoneNumberConfirmation(commit: { sharePhoneNumber in
|
|
component.completion(accountContext, accountPeer, .accept(allowWriteAccess: allowWrite, sharePhoneNumber: sharePhoneNumber, matchCode: matchCode))
|
|
state.inProgress = true
|
|
state.selectedMatchCode = matchCode
|
|
state.updated()
|
|
})
|
|
} else {
|
|
component.completion(accountContext, accountPeer, .accept(allowWriteAccess: allowWrite, sharePhoneNumber: false, matchCode: matchCode))
|
|
state.inProgress = true
|
|
state.selectedMatchCode = matchCode
|
|
state.updated()
|
|
}
|
|
}
|
|
|
|
var contentHeight: CGFloat = 32.0
|
|
|
|
if state.displayEmoji, let matchCodes = state.matchCodes {
|
|
contentHeight += 36.0
|
|
|
|
let emojiTitle = emojiTitle.update(
|
|
component: MultilineTextComponent(
|
|
text: .markdown(text: strings.AuthConfirmation_Emoji_Title, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 2,
|
|
lineSpacing: 0.2
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(emojiTitle
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + emojiTitle.size.height / 2.0))
|
|
.appear(.default(scale: false, alpha: true))
|
|
.disappear(.default(scale: false, alpha: true))
|
|
)
|
|
contentHeight += emojiTitle.size.height
|
|
contentHeight += 16.0
|
|
|
|
let emojiDescription = emojiDescription.update(
|
|
component: MultilineTextComponent(
|
|
text: .markdown(text: strings.AuthConfirmation_Emoji_Description, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 3,
|
|
lineSpacing: 0.2
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(emojiDescription
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + emojiDescription.size.height / 2.0))
|
|
.appear(.default(scale: false, alpha: true))
|
|
.disappear(.default(scale: false, alpha: true))
|
|
)
|
|
contentHeight += emojiDescription.size.height
|
|
contentHeight += 48.0
|
|
|
|
var emojiDelay: Double = 0.0
|
|
let emojiSize = CGSize(width: 64.0, height: 64.0)
|
|
let emojiSpacing: CGFloat = 36.0
|
|
let totalWidth = CGFloat(matchCodes.count) * emojiSize.width + CGFloat(matchCodes.count - 1) * emojiSpacing
|
|
var emojiOriginX = context.availableSize.width / 2.0 - totalWidth / 2.0
|
|
for code in matchCodes {
|
|
var items: [AnyComponentWithIdentity<Empty>] = []
|
|
items.append(
|
|
AnyComponentWithIdentity(id: "background", component: AnyComponent(
|
|
FilledRoundedRectangleComponent(color: theme.list.itemBlocksBackgroundColor, cornerRadius: .minEdge, smoothCorners: false)
|
|
))
|
|
)
|
|
if state.selectedMatchCode == code {
|
|
items.append(
|
|
AnyComponentWithIdentity(id: "progress", component: AnyComponent(
|
|
ActivityIndicatorComponent(color: theme.list.itemAccentColor)
|
|
))
|
|
)
|
|
}
|
|
|
|
var file: TelegramMediaFile?
|
|
if let item = component.context.animatedEmojiStickersValue[code] {
|
|
file = item.first?.file._parse()
|
|
} else if let item = component.context.animatedEmojiStickersValue[code.strippedEmoji] {
|
|
file = item.first?.file._parse()
|
|
}
|
|
if let file {
|
|
items.append(
|
|
AnyComponentWithIdentity(id: "animatedIcon", component: AnyComponent(
|
|
LottieComponent(content: LottieComponent.ResourceContent(context: component.context, file: file, attemptSynchronously: true, providesPlaceholder: true), placeholderColor: theme.list.mediaPlaceholderColor, startingPosition: .begin, size: CGSize(width: 32.0, height: 32.0), loop: true, playOnce: nil)
|
|
))
|
|
)
|
|
} else {
|
|
items.append(
|
|
AnyComponentWithIdentity(id: "staticIcon", component: AnyComponent(
|
|
Text(text: code, font: Font.regular(32.0), color: .black)
|
|
))
|
|
)
|
|
}
|
|
|
|
let subject = component.subject
|
|
let emoji = emojis[code].update(
|
|
component: AnyComponent(
|
|
PlainButtonComponent(
|
|
content: AnyComponent(
|
|
ZStack(items)
|
|
),
|
|
minSize: emojiSize,
|
|
action: { [weak state] in
|
|
guard let state else {
|
|
return
|
|
}
|
|
if case let .request(_, _, _, flags, _, _) = subject, flags.contains(.showMatchCodesFirst) {
|
|
state.checkMatchCode(code)
|
|
} else {
|
|
complete(code)
|
|
}
|
|
},
|
|
isEnabled: state.selectedMatchCode == nil,
|
|
animateAlpha: false,
|
|
animateScale: true
|
|
)
|
|
),
|
|
environment: {},
|
|
availableSize: emojiSize,
|
|
transition: context.transition
|
|
)
|
|
context.add(emoji
|
|
.position(CGPoint(x: emojiOriginX + emojiSize.width / 2.0, y: contentHeight + emojiSize.height / 2.0))
|
|
.opacity(state.selectedMatchCode != nil && state.selectedMatchCode != code ? 0.6 : 1.0)
|
|
.appear(ComponentTransition.Appear({ _, view, transition in
|
|
if !transition.animation.isImmediate {
|
|
transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: emojiDelay)
|
|
transition.animateScale(view: view, from: 0.01, to: 1.0, delay: emojiDelay)
|
|
}
|
|
}))
|
|
.disappear(.default(scale: true, alpha: true))
|
|
)
|
|
emojiOriginX += emojiSize.width + emojiSpacing
|
|
emojiDelay += 0.08
|
|
}
|
|
|
|
contentHeight += emojiSize.height
|
|
contentHeight += 48.0
|
|
} else {
|
|
let avatar = avatar.update(
|
|
component: AvatarComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
peer: EnginePeer(bot),
|
|
clipStyle: .roundedRect
|
|
),
|
|
availableSize: CGSize(width: 92.0, height: 92.0),
|
|
transition: .immediate
|
|
)
|
|
context.add(avatar
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + avatar.size.height / 2.0))
|
|
.appear(.default(scale: false, alpha: true))
|
|
.disappear(.default(scale: false, alpha: true))
|
|
)
|
|
contentHeight += avatar.size.height
|
|
contentHeight += 18.0
|
|
|
|
let title = title.update(
|
|
component: MultilineTextComponent(
|
|
text: .markdown(text: strings.AuthConfirmation_Title(domain).string, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 2
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(title
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0))
|
|
.appear(.default(scale: false, alpha: true))
|
|
.disappear(.default(scale: false, alpha: true))
|
|
)
|
|
contentHeight += title.size.height
|
|
contentHeight += 16.0
|
|
|
|
let description = description.update(
|
|
component: MultilineTextComponent(
|
|
text: .markdown(text: strings.AuthConfirmation_Description, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 3,
|
|
lineSpacing: 0.2
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(description
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + description.size.height / 2.0))
|
|
.appear(.default(scale: false, alpha: true))
|
|
.disappear(.default(scale: false, alpha: true))
|
|
)
|
|
contentHeight += description.size.height
|
|
contentHeight += 16.0
|
|
|
|
var clientSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
|
clientSectionItems.append(
|
|
AnyComponentWithIdentity(id: "device", component: AnyComponent(
|
|
ListActionItemComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
title: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: strings.AuthConfirmation_Device,
|
|
font: Font.regular(17.0),
|
|
textColor: theme.list.itemPrimaryTextColor
|
|
)),
|
|
maximumNumberOfLines: 1
|
|
)),
|
|
contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0),
|
|
accessory: .custom(ListActionItemComponent.CustomAccessory(
|
|
component: AnyComponentWithIdentity(
|
|
id: "info",
|
|
component: AnyComponent(
|
|
VStack([
|
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: clientData?.platform ?? "",
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
|
|
textColor: theme.list.itemPrimaryTextColor
|
|
)),
|
|
maximumNumberOfLines: 1
|
|
))),
|
|
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: clientData?.browser ?? "",
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0),
|
|
textColor: theme.list.itemSecondaryTextColor
|
|
)),
|
|
horizontalAlignment: .left,
|
|
truncationType: .middle,
|
|
maximumNumberOfLines: 1
|
|
)))
|
|
], alignment: .right, spacing: 3.0)
|
|
)
|
|
),
|
|
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
|
|
isInteractive: true
|
|
)),
|
|
action: nil
|
|
)
|
|
))
|
|
)
|
|
|
|
clientSectionItems.append(
|
|
AnyComponentWithIdentity(id: "region", component: AnyComponent(
|
|
ListActionItemComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
title: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: strings.AuthConfirmation_IpAddress,
|
|
font: Font.regular(17.0),
|
|
textColor: theme.list.itemPrimaryTextColor
|
|
)),
|
|
maximumNumberOfLines: 1
|
|
)),
|
|
contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0),
|
|
accessory: .custom(ListActionItemComponent.CustomAccessory(
|
|
component: AnyComponentWithIdentity(
|
|
id: "info",
|
|
component: AnyComponent(
|
|
VStack([
|
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: clientData?.ip ?? "",
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
|
|
textColor: theme.list.itemPrimaryTextColor
|
|
)),
|
|
maximumNumberOfLines: 1
|
|
))),
|
|
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: clientData?.region ?? "",
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0),
|
|
textColor: theme.list.itemSecondaryTextColor
|
|
)),
|
|
horizontalAlignment: .left,
|
|
truncationType: .middle,
|
|
maximumNumberOfLines: 1
|
|
)))
|
|
], alignment: .right, spacing: 3.0)
|
|
)
|
|
),
|
|
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
|
|
isInteractive: true
|
|
)),
|
|
action: nil
|
|
)
|
|
))
|
|
)
|
|
|
|
let clientSection = clientSection.update(
|
|
component: ListSectionComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
header: nil,
|
|
footer: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: strings.AuthConfirmation_Info,
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
|
textColor: environment.theme.list.freeTextColor
|
|
)),
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
items: clientSectionItems
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
|
transition: context.transition
|
|
)
|
|
context.add(clientSection
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + clientSection.size.height / 2.0))
|
|
.appear(.default(scale: false, alpha: true))
|
|
.disappear(.default(scale: false, alpha: true))
|
|
)
|
|
contentHeight += clientSection.size.height
|
|
|
|
if flags.contains(.requestWriteAccess) {
|
|
contentHeight += 38.0
|
|
|
|
var optionsSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
|
optionsSectionItems.append(AnyComponentWithIdentity(id: "allowWrite", component: AnyComponent(ListActionItemComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
title: AnyComponent(VStack([
|
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: strings.AuthConfirmation_AllowMessages,
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
|
|
textColor: theme.list.itemPrimaryTextColor
|
|
)),
|
|
maximumNumberOfLines: 1
|
|
))),
|
|
], alignment: .left, spacing: 2.0)),
|
|
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: state.allowWrite, action: { [weak state] _ in
|
|
guard let state else {
|
|
return
|
|
}
|
|
state.allowWrite = !state.allowWrite
|
|
state.updated()
|
|
})),
|
|
action: nil
|
|
))))
|
|
let optionsSection = optionsSection.update(
|
|
component: ListSectionComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
header: nil,
|
|
footer: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: strings.AuthConfirmation_AllowMessagesInfo(EnginePeer(bot).compactDisplayTitle).string,
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
|
textColor: environment.theme.list.freeTextColor
|
|
)),
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
items: optionsSectionItems
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
|
transition: context.transition
|
|
)
|
|
context.add(optionsSection
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + optionsSection.size.height / 2.0))
|
|
.appear(.default(scale: false, alpha: true))
|
|
.disappear(.default(scale: false, alpha: true))
|
|
)
|
|
contentHeight += optionsSection.size.height
|
|
}
|
|
contentHeight += 32.0
|
|
}
|
|
|
|
let buttonSpacing: CGFloat = 10.0
|
|
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
|
|
var buttonWidth = context.availableSize.width - buttonInsets.left - buttonInsets.right
|
|
if !state.displayEmoji {
|
|
buttonWidth = (buttonWidth - buttonSpacing) / 2.0
|
|
}
|
|
|
|
let cancelButton = cancelButton.update(
|
|
component: ButtonComponent(
|
|
background: ButtonComponent.Background(
|
|
style: .glass,
|
|
color: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1),
|
|
foreground: theme.list.itemPrimaryTextColor,
|
|
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.AuthConfirmation_Cancel, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center))))
|
|
),
|
|
action: { [weak state] in
|
|
guard let state else {
|
|
return
|
|
}
|
|
let accountContext = state.forcedAccount?.0 ?? component.context
|
|
guard let accountPeer = state.forcedAccount?.1 ?? state.peer else {
|
|
return
|
|
}
|
|
component.completion(accountContext, accountPeer, .decline)
|
|
component.cancel(true)
|
|
}
|
|
),
|
|
availableSize: CGSize(width: buttonWidth, height: 52.0),
|
|
transition: context.transition
|
|
)
|
|
context.add(cancelButton
|
|
.position(CGPoint(x: state.displayEmoji ? context.availableSize.width / 2.0 : context.availableSize.width / 2.0 - buttonSpacing / 2.0 - cancelButton.size.width / 2.0, y: contentHeight + cancelButton.size.height / 2.0))
|
|
)
|
|
|
|
let doneButton = doneButton.update(
|
|
component: ButtonComponent(
|
|
background: ButtonComponent.Background(
|
|
style: .glass,
|
|
color: theme.list.itemCheckColors.fillColor,
|
|
foreground: theme.list.itemCheckColors.foregroundColor,
|
|
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
|
cornerRadius: 10.0,
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.AuthConfirmation_LogIn, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
|
|
),
|
|
displaysProgress: state.inProgress,
|
|
action: { [weak state] in
|
|
guard let state else {
|
|
return
|
|
}
|
|
if !flags.contains(.showMatchCodesFirst), let matchCodes, !matchCodes.isEmpty {
|
|
state.displayEmoji = true
|
|
state.matchCodes = matchCodes.shuffled()
|
|
state.updated(transition: .spring(duration: 0.4))
|
|
} else {
|
|
complete(state.selectedMatchCode)
|
|
}
|
|
}
|
|
),
|
|
availableSize: CGSize(width: buttonWidth, height: 52.0),
|
|
transition: context.transition
|
|
)
|
|
context.add(doneButton
|
|
.position(CGPoint(x: context.availableSize.width / 2.0 + buttonSpacing / 2.0 + doneButton.size.width / 2.0, y: contentHeight + doneButton.size.height / 2.0))
|
|
.opacity(state.displayEmoji ? 0.0 : 1.0)
|
|
.scale(state.displayEmoji ? 0.01 : 1.0)
|
|
)
|
|
|
|
contentHeight += cancelButton.size.height
|
|
contentHeight += buttonInsets.bottom
|
|
|
|
return CGSize(width: context.availableSize.width, height: contentHeight)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class AuthConfirmationSheetComponent: CombinedComponent {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let requestSubject: MessageActionUrlSubject
|
|
let subject: MessageActionUrlAuthResult
|
|
let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
requestSubject: MessageActionUrlSubject,
|
|
subject: MessageActionUrlAuthResult,
|
|
completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
|
|
) {
|
|
self.context = context
|
|
self.requestSubject = requestSubject
|
|
self.subject = subject
|
|
self.completion = completion
|
|
}
|
|
|
|
static func ==(lhs: AuthConfirmationSheetComponent, rhs: AuthConfirmationSheetComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
static var body: Body {
|
|
let sheet = Child(SheetComponent<EnvironmentType>.self)
|
|
let animateOut = StoredActionSlot(Action<Void>.self)
|
|
|
|
return { context in
|
|
let environment = context.environment[EnvironmentType.self]
|
|
let controller = environment.controller
|
|
|
|
let sheet = sheet.update(
|
|
component: SheetComponent<EnvironmentType>(
|
|
content: AnyComponent<EnvironmentType>(AuthConfirmationSheetContent(
|
|
context: context.component.context,
|
|
requestSubject: context.component.requestSubject,
|
|
subject: context.component.subject,
|
|
completion: context.component.completion,
|
|
cancel: { animate in
|
|
if animate {
|
|
animateOut.invoke(Action { _ in
|
|
if let controller = controller() {
|
|
controller.dismiss(completion: nil)
|
|
}
|
|
})
|
|
} else if let controller = controller() {
|
|
controller.dismiss(animated: false, completion: nil)
|
|
}
|
|
}
|
|
)),
|
|
style: .glass,
|
|
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
|
|
followContentSizeChanges: true,
|
|
clipsContent: true,
|
|
animateOut: animateOut
|
|
),
|
|
environment: {
|
|
environment
|
|
SheetComponentEnvironment(
|
|
metrics: environment.metrics,
|
|
deviceMetrics: environment.deviceMetrics,
|
|
isDisplaying: environment.value.isVisible,
|
|
isCentered: environment.metrics.widthClass == .regular,
|
|
hasInputHeight: !environment.inputHeight.isZero,
|
|
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
|
dismiss: { animated in
|
|
if animated {
|
|
animateOut.invoke(Action { _ in
|
|
if let controller = controller() {
|
|
controller.dismiss(completion: nil)
|
|
}
|
|
})
|
|
} else {
|
|
if let controller = controller() {
|
|
controller.dismiss(completion: nil)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
},
|
|
availableSize: context.availableSize,
|
|
transition: context.transition
|
|
)
|
|
|
|
context.add(sheet
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
|
)
|
|
|
|
return context.availableSize
|
|
}
|
|
}
|
|
}
|
|
|
|
public class AuthConfirmationScreen: ViewControllerComponentContainer {
|
|
public enum Result {
|
|
case accept(allowWriteAccess: Bool, sharePhoneNumber: Bool, matchCode: String?)
|
|
case decline
|
|
case failed
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let requestSubject: MessageActionUrlSubject
|
|
private let subject: MessageActionUrlAuthResult
|
|
fileprivate let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
requestSubject: MessageActionUrlSubject,
|
|
subject: MessageActionUrlAuthResult,
|
|
completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
|
|
) {
|
|
self.context = context
|
|
self.requestSubject = requestSubject
|
|
self.subject = subject
|
|
self.completion = completion
|
|
|
|
super.init(
|
|
context: context,
|
|
component: AuthConfirmationSheetComponent(
|
|
context: context,
|
|
requestSubject: requestSubject,
|
|
subject: subject,
|
|
completion: completion
|
|
),
|
|
navigationBarAppearance: .none,
|
|
statusBarStyle: .ignore,
|
|
theme: .default
|
|
)
|
|
|
|
self.navigationPresentation = .flatModal
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
self.view.disablesInteractiveModalDismiss = true
|
|
}
|
|
|
|
public func dismissAnimated() {
|
|
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
|
view.dismissAnimated()
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class AuthConfirmationReferenceContentSource: ContextReferenceContentSource {
|
|
private let controller: ViewController
|
|
private let sourceView: UIView
|
|
|
|
let forceDisplayBelowKeyboard = true
|
|
|
|
init(controller: ViewController, sourceView: UIView) {
|
|
self.controller = controller
|
|
self.sourceView = sourceView
|
|
}
|
|
|
|
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
|
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
}
|
|
|
|
private final class ActivityIndicatorComponent: Component {
|
|
let color: UIColor
|
|
|
|
init(
|
|
color: UIColor
|
|
) {
|
|
self.color = color
|
|
}
|
|
|
|
static func ==(lhs: ActivityIndicatorComponent, rhs: ActivityIndicatorComponent) -> Bool {
|
|
if lhs.color != rhs.color {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let background = UIView()
|
|
private let activityIndicator: ActivityIndicator
|
|
|
|
private var component: ActivityIndicatorComponent?
|
|
|
|
override init(frame: CGRect) {
|
|
self.activityIndicator = ActivityIndicator(type: .custom(.white, 64.0, 2.0, true))
|
|
|
|
super.init(frame: frame)
|
|
|
|
|
|
self.addSubview(self.background)
|
|
self.addSubview(self.activityIndicator.view)
|
|
}
|
|
|
|
required public init(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: ActivityIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
|
let size = CGSize(width: 64.0, height: 64.0)
|
|
|
|
self.background.backgroundColor = component.color.withMultipliedAlpha(0.1)
|
|
self.background.layer.cornerRadius = 32.0
|
|
self.background.clipsToBounds = true
|
|
|
|
if self.component == nil {
|
|
self.activityIndicator.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
}
|
|
self.component = component
|
|
|
|
self.background.frame = CGRect(origin: .zero, size: size)
|
|
self.activityIndicator.frame = CGRect(origin: .zero, size: size)
|
|
self.activityIndicator.type = .custom(component.color, 64.0, 2.0, true)
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
|
}
|
|
}
|