no message

This commit is contained in:
Peter 2018-09-11 22:20:43 +03:00
parent 3a414eeea2
commit 0648840a26
23 changed files with 749 additions and 247 deletions

View File

@ -4,10 +4,14 @@ import AVFoundation
import Postbox
import SwiftSignalKit
final class CallKitIntegration {
public final class CallKitIntegration {
private let providerDelegate: AnyObject
public static var isAvailable: Bool {
#if (arch(i386) || arch(x86_64)) && os(iOS)
return false
#endif
if #available(iOSApplicationExtension 10.0, *) {
return Locale.current.regionCode?.lowercased() != "cn"
} else {

View File

@ -406,7 +406,7 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P
typesString.append(strings.Notification_PassportValueEmail)
}
}
attributedString = NSAttributedString(string: strings.Notification_PassportValuesSentMessage(message.author?.compactDisplayTitle ?? "", typesString).0, font: titleFont, textColor: primaryTextColor)
attributedString = NSAttributedString(string: strings.Notification_PassportValuesSentMessage(message.peers[message.id.peerId]?.compactDisplayTitle ?? "", typesString).0, font: titleFont, textColor: primaryTextColor)
case .unknown:
attributedString = nil
}

View File

@ -13,6 +13,7 @@ import LegacyComponents
public enum DeviceAccessMicrophoneSubject {
case audio
case video
case voiceCall
}
public enum DeviceAccessMediaLibrarySubject {
@ -48,7 +49,7 @@ public final class DeviceAccess {
return self.contactsPromise.get()
}
public static func authorizeAccess(to subject: DeviceAccessSubject, presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void, openSettings: @escaping () -> Void, _ completion: @escaping (Bool) -> Void) {
public static func authorizeAccess(to subject: DeviceAccessSubject, presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void, openSettings: @escaping () -> Void, displayNotificatoinFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void) {
switch subject {
case .camera:
let status = PGCamera.cameraAuthorizationStatus()
@ -96,10 +97,15 @@ public final class DeviceAccess {
text = presentationData.strings.AccessDenied_VoiceMicrophone
case .video:
text = presentationData.strings.AccessDenied_VideoMicrophone
case .voiceCall:
text = presentationData.strings.AccessDenied_CallMicrophone
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
if case .voiceCall = microphoneSubject {
displayNotificatoinFromBackground(text)
}
}
})
}

View File

@ -15,6 +15,7 @@ private var telegramUIDeclaredEncodables: Void = {
declareEncodable(ApplicationSpecificVariantNotice.self, f: { ApplicationSpecificVariantNotice(decoder: $0) })
declareEncodable(ApplicationSpecificCounterNotice.self, f: { ApplicationSpecificCounterNotice(decoder: $0) })
declareEncodable(CallListSettings.self, f: { CallListSettings(decoder: $0) })
declareEncodable(VoiceCallSettings.self, f: { VoiceCallSettings(decoder: $0) })
declareEncodable(ExperimentalSettings.self, f: { ExperimentalSettings(decoder: $0) })
declareEncodable(ExperimentalUISettings.self, f: { ExperimentalUISettings(decoder: $0) })
declareEncodable(MusicPlaybackSettings.self, f: { MusicPlaybackSettings(decoder: $0) })

View File

@ -1,10 +1,10 @@
import Foundation
import TelegramCore
func findValue(_ values: [SecureIdValueWithContext], key: SecureIdValueKey) -> (Int, SecureIdValue)? {
func findValue(_ values: [SecureIdValueWithContext], key: SecureIdValueKey) -> (Int, SecureIdValueWithContext)? {
for i in 0 ..< values.count {
if values[i].value.key == key {
return (i, values[i].value)
return (i, values[i])
}
}
return nil

View File

@ -419,8 +419,9 @@ class ItemListController<Entry: ItemListNodeEntry>: ViewController {
}
}
self.didAppear?(!self.didAppearOnce)
let firstTime = !self.didAppearOnce
self.didAppearOnce = true
self.didAppear?(firstTime)
}
override func viewWillDisappear(_ animated: Bool) {

View File

@ -331,19 +331,23 @@ class ItemListControllerNode<Entry: ItemListNodeEntry>: ViewControllerTracingNod
updatedFocusItemTag = true
}
if updatedFocusItemTag {
strongSelf.appliedFocusItemTag = focusItemTag
if let focusItemTag = focusItemTag {
var applied = false
strongSelf.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode {
if let itemTag = itemNode.tag {
if itemTag.isEqual(to: focusItemTag) {
if let focusableNode = itemNode as? ItemListItemFocusableNode {
applied = true
focusableNode.focus()
}
}
}
}
}
if applied {
strongSelf.appliedFocusItemTag = focusItemTag
}
}
}
}

View File

@ -420,6 +420,7 @@ private final class AudioPlayerRendererContext {
var maximumFramesPerSlice: UInt32 = 4096
AudioUnitSetProperty(converterAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
AudioUnitSetProperty(timePitchAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
guard AUGraphInitialize(audioGraph) == noErr else {
return

View File

@ -2,6 +2,7 @@ import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
import AVFoundation
public enum PresentationCallState: Equatable {
@ -176,11 +177,12 @@ public final class PresentationCall {
private let audioSession: ManagedAudioSession
private let callSessionManager: CallSessionManager
private let callKitIntegration: CallKitIntegration?
private let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void)
let internalId: CallSessionInternalId
let peerId: PeerId
let isOutgoing: Bool
let peer: Peer?
public let internalId: CallSessionInternalId
public let peerId: PeerId
public let isOutgoing: Bool
public let peer: Peer?
private var sessionState: CallSession?
private var callContextState: OngoingCallContextState?
@ -231,10 +233,11 @@ public final class PresentationCall {
private var droppedCall = false
private var dropCallKitCallTimer: SwiftSignalKit.Timer?
init(audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, allowP2P: Bool, proxyServer: ProxyServerSettings?, currentNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>) {
init(audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, allowP2P: Bool, proxyServer: ProxyServerSettings?, currentNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>) {
self.audioSession = audioSession
self.callSessionManager = callSessionManager
self.callKitIntegration = callKitIntegration
self.getDeviceAccessData = getDeviceAccessData
self.internalId = internalId
self.peerId = peerId
@ -542,8 +545,23 @@ public final class PresentationCall {
}
func answer() {
self.callSessionManager.accept(internalId: self.internalId)
self.callKitIntegration?.answerCall(uuid: self.internalId)
let (presentationData, present, openSettings) = self.getDeviceAccessData()
DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in
present(c, a)
}, openSettings: {
openSettings()
}, { [weak self] value in
guard let strongSelf = self else {
return
}
if value {
strongSelf.callSessionManager.accept(internalId: strongSelf.internalId)
strongSelf.callKitIntegration?.answerCall(uuid: strongSelf.internalId)
} else {
let _ = strongSelf.hangUp().start()
}
})
}
func hangUp() -> Signal<Bool, NoError> {

View File

@ -2,10 +2,14 @@ import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
private func p2pAllowed(settings: VoiceCallSettings?, isContact: Bool) -> Bool {
let mode = settings?.p2pMode ?? .contacts
switch (mode, isContact) {
private func p2pAllowed(settings: (VoiceCallSettings, VoipConfiguration)?, isContact: Bool) -> Bool {
var mode: VoiceCallP2PMode? = settings?.0.p2pMode
if mode == nil {
mode = settings?.1.defaultP2PMode
}
switch (mode ?? .contacts, isContact) {
case (.always, _), (.contacts, true):
return true
default:
@ -42,6 +46,7 @@ public enum RequestCallResult {
public final class PresentationCallManager {
private let postbox: Postbox
private let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void)
private let networkType: Signal<NetworkType, NoError>
private let audioSession: ManagedAudioSession
private let callSessionManager: CallSessionManager
@ -67,11 +72,12 @@ public final class PresentationCallManager {
private var proxyServer: ProxyServerSettings?
private var proxyServerDisposable: Disposable?
private var callSettings: VoiceCallSettings?
private var callSettings: (VoiceCallSettings, VoipConfiguration)?
private var callSettingsDisposable: Disposable?
public init(postbox: Postbox, networkType: Signal<NetworkType, NoError>, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager) {
public init(postbox: Postbox, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), networkType: Signal<NetworkType, NoError>, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager) {
self.postbox = postbox
self.getDeviceAccessData = getDeviceAccessData
self.networkType = networkType
self.audioSession = audioSession
self.callSessionManager = callSessionManager
@ -99,39 +105,46 @@ public final class PresentationCallManager {
audioSessionActivationChangedImpl?(value)
})
self.ringingStatesDisposable = (callSessionManager.ringingStates()
|> mapToSignal { ringingStates -> Signal<[(Peer, CallSessionRingingState, Bool)], NoError> in
let enableCallKit = postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.voiceCallSettings])
|> map { preferences -> Bool in
let settings = preferences.values[ApplicationSpecificPreferencesKeys.voiceCallSettings] as? VoiceCallSettings ?? .defaultSettings
return settings.enableSystemIntegration
}
|> distinctUntilChanged
self.ringingStatesDisposable = (combineLatest(callSessionManager.ringingStates(), enableCallKit)
|> mapToSignal { ringingStates, enableCallKit -> Signal<([(Peer, CallSessionRingingState, Bool)], Bool), NoError> in
if ringingStates.isEmpty {
return .single([])
return .single(([], enableCallKit))
} else {
return postbox.transaction { transaction -> [(Peer, CallSessionRingingState, Bool)] in
return postbox.transaction { transaction -> ([(Peer, CallSessionRingingState, Bool)], Bool) in
var result: [(Peer, CallSessionRingingState, Bool)] = []
for state in ringingStates {
if let peer = transaction.getPeer(state.peerId) {
result.append((peer, state, transaction.isPeerContact(peerId: state.peerId)))
}
}
return result
return (result, enableCallKit)
}
}
}
|> mapToSignal { states -> Signal<([(Peer, CallSessionRingingState, Bool)], NetworkType), NoError> in
|> mapToSignal { states, enableCallKit -> Signal<([(Peer, CallSessionRingingState, Bool)], NetworkType, Bool), NoError> in
return networkType
|> take(1)
|> map { currentNetworkType -> ([(Peer, CallSessionRingingState, Bool)], NetworkType) in
return (states, currentNetworkType)
|> map { currentNetworkType -> ([(Peer, CallSessionRingingState, Bool)], NetworkType, Bool) in
return (states, currentNetworkType, enableCallKit)
}
}
|> deliverOnMainQueue).start(next: { [weak self] ringingStates, currentNetworkType in
self?.ringingStatesUpdated(ringingStates, currentNetworkType: currentNetworkType)
|> deliverOnMainQueue).start(next: { [weak self] ringingStates, currentNetworkType, enableCallKit in
self?.ringingStatesUpdated(ringingStates, currentNetworkType: currentNetworkType, enableCallKit: enableCallKit)
})
startCallImpl = { [weak self] uuid, handle in
if let strongSelf = self, let userId = Int32(handle) {
return strongSelf.startCall(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), internalId: uuid)
|> take(1)
|> map { _ -> Bool in
return true
|> map { result -> Bool in
return result
}
} else {
return .single(false)
@ -171,10 +184,12 @@ public final class PresentationCallManager {
}
})
self.callSettingsDisposable = (postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.voiceCallSettings])
self.callSettingsDisposable = (postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.voiceCallSettings, PreferencesKeys.voipConfiguration])
|> deliverOnMainQueue).start(next: { [weak self] preferences in
if let strongSelf = self, let settings = preferences.values[ApplicationSpecificPreferencesKeys.voiceCallSettings] as? VoiceCallSettings {
strongSelf.callSettings = settings
let callSettings = preferences.values[ApplicationSpecificPreferencesKeys.voiceCallSettings] as? VoiceCallSettings ?? .defaultSettings
let configuration = preferences.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? .defaultValue
if let strongSelf = self {
strongSelf.callSettings = (callSettings, configuration)
}
})
}
@ -187,10 +202,10 @@ public final class PresentationCallManager {
self.callSettingsDisposable?.dispose()
}
private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState, Bool)], currentNetworkType: NetworkType) {
private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState, Bool)], currentNetworkType: NetworkType, enableCallKit: Bool) {
if let firstState = ringingStates.first {
if self.currentCall == nil {
let call = PresentationCall(audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(self.callKitIntegration, settings: self.callSettings), internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0, allowP2P: p2pAllowed(settings: self.callSettings, isContact: firstState.2), proxyServer: self.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: self.networkType)
let call = PresentationCall(audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: enableCallKit ? self.callKitIntegration : nil, getDeviceAccessData: self.getDeviceAccessData, internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0, allowP2P: p2pAllowed(settings: self.callSettings, isContact: firstState.2), proxyServer: self.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: self.networkType)
self.currentCall = call
self.currentCallPromise.set(.single(call))
self.hasActiveCallsPromise.set(true)
@ -213,12 +228,35 @@ public final class PresentationCallManager {
return .alreadyInProgress(call.peerId)
}
if let _ = self.callKitIntegration {
startCallDisposable.set((postbox.loadedPeerWithId(peerId)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self {
strongSelf.callKitIntegration?.startCall(peerId: peerId, displayTitle: peer.displayTitle)
let (presentationData, present, openSettings) = self.getDeviceAccessData()
let accessEnabledSignal: Signal<Bool, NoError> = Signal { subscriber in
DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in
present(c, a)
}, openSettings: {
openSettings()
}, { value in
subscriber.putNext(value)
subscriber.putCompletion()
})
return EmptyDisposable
}
|> runOn(Queue.mainQueue())
let postbox = self.postbox
self.startCallDisposable.set((accessEnabledSignal
|> mapToSignal { accessEnabled -> Signal<Peer?, NoError> in
if !accessEnabled {
return .single(nil)
}
return postbox.loadedPeerWithId(peerId)
|> take(1)
|> map(Optional.init)
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self, let peer = peer else {
return
}
strongSelf.callKitIntegration?.startCall(peerId: peerId, displayTitle: peer.displayTitle)
}))
} else {
let _ = self.startCall(peerId: peerId).start()
@ -227,17 +265,40 @@ public final class PresentationCallManager {
}
private func startCall(peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<Bool, NoError> {
return (combineLatest(self.callSessionManager.request(peerId: peerId, internalId: internalId), self.networkType |> take(1), postbox.peerView(id: peerId) |> take(1) |> map({ peerView -> Bool in
let (presentationData, present, openSettings) = self.getDeviceAccessData()
let accessEnabledSignal: Signal<Bool, NoError> = Signal { subscriber in
DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in
present(c, a)
}, openSettings: {
openSettings()
}, { value in
subscriber.putNext(value)
subscriber.putCompletion()
})
return EmptyDisposable
}
|> runOn(Queue.mainQueue())
let postbox = self.postbox
let callSessionManager = self.callSessionManager
let networkType = self.networkType
return accessEnabledSignal
|> mapToSignal { [weak self] accessEnabled -> Signal<Bool, NoError> in
if !accessEnabled {
return .single(false)
}
return (combineLatest(callSessionManager.request(peerId: peerId, internalId: internalId), networkType |> take(1), postbox.peerView(id: peerId) |> take(1) |> map({ peerView -> Bool in
return peerView.peerIsContact
}))
}) |> take(1))
|> deliverOnMainQueue
|> beforeNext { [weak self] internalId, currentNetworkType, isContact in
if let strongSelf = self {
|> beforeNext { internalId, currentNetworkType, isContact in
if let strongSelf = self, accessEnabled {
if let currentCall = strongSelf.currentCall {
currentCall.rejectBusy()
}
let call = PresentationCall(audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings), internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, allowP2P: p2pAllowed(settings: strongSelf.callSettings, isContact: isContact), proxyServer: strongSelf.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: strongSelf.networkType)
let call = PresentationCall(audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings?.0), getDeviceAccessData: strongSelf.getDeviceAccessData, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, allowP2P: p2pAllowed(settings: strongSelf.callSettings, isContact: isContact), proxyServer: strongSelf.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: strongSelf.networkType)
strongSelf.currentCall = call
strongSelf.currentCallPromise.set(.single(call))
strongSelf.hasActiveCallsPromise.set(true)
@ -253,8 +314,9 @@ public final class PresentationCallManager {
}))
}
})
|> mapToSignal { _ -> Signal<Bool, NoError> in
|> mapToSignal { value -> Signal<Bool, NoError> in
return .single(true)
}
}
}
}

View File

@ -412,22 +412,19 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign
let privacySignal = privacySettingsPromise.get()
|> take(1)
let callsSignal = account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.voiceCallSettings])
let callsSignal = account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.voiceCallSettings, PreferencesKeys.voipConfiguration])
|> take(1)
|> map { view -> VoiceCallSettings in
let voiceCallSettings: VoiceCallSettings
if let value = view.values[ApplicationSpecificPreferencesKeys.voiceCallSettings] as? VoiceCallSettings {
voiceCallSettings = value
} else {
voiceCallSettings = VoiceCallSettings.defaultSettings
|> map { view -> (VoiceCallSettings, VoipConfiguration) in
let voiceCallSettings: VoiceCallSettings = view.values[ApplicationSpecificPreferencesKeys.voiceCallSettings] as? VoiceCallSettings ?? .defaultSettings
let voipConfiguration = view.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? .defaultValue
return (voiceCallSettings, voipConfiguration)
}
return voiceCallSettings
}
currentInfoDisposable.set((combineLatest(privacySignal, callsSignal) |> deliverOnMainQueue).start(next: { [weak currentInfoDisposable] info, callSettings in
currentInfoDisposable.set((combineLatest(privacySignal, callsSignal)
|> deliverOnMainQueue).start(next: { [weak currentInfoDisposable] info, callSettings in
if let info = info {
pushControllerImpl?(selectivePrivacySettingsController(account: account, kind: .voiceCalls, current: info.voiceCalls, callSettings: callSettings, callIntegrationAvailable: CallKitIntegration.isAvailable, updated: { updated, updatedCallSettings in
pushControllerImpl?(selectivePrivacySettingsController(account: account, kind: .voiceCalls, current: info.voiceCalls, callSettings: callSettings.0, voipConfiguration: callSettings.1, callIntegrationAvailable: CallKitIntegration.isAvailable, updated: { updated, updatedCallSettings in
if let currentInfoDisposable = currentInfoDisposable, let updatedCallSettings = updatedCallSettings {
let _ = updateVoiceCallSettingsSettingsInteractively(postbox: account.postbox, { _ in
return updatedCallSettings

View File

@ -46,6 +46,7 @@ final class SecureIdAuthController: ViewController {
private var didPlayPresentationAnimation = false
private let challengeDisposable = MetaDisposable()
private let authenthicateDisposable = MetaDisposable()
private var formDisposable: Disposable?
private let deleteDisposable = MetaDisposable()
private let recoveryDisposable = MetaDisposable()
@ -62,9 +63,9 @@ final class SecureIdAuthController: ViewController {
switch mode {
case .form:
self.state = .form(SecureIdAuthControllerFormState(encryptedFormData: nil, formData: nil, verificationState: nil))
self.state = .form(SecureIdAuthControllerFormState(encryptedFormData: nil, formData: nil, verificationState: nil, removingValues: false))
case .list:
self.state = .list(SecureIdAuthControllerListState(verificationState: nil, encryptedValues: nil, primaryLanguageByCountry: [:], values: nil))
self.state = .list(SecureIdAuthControllerListState(verificationState: nil, encryptedValues: nil, primaryLanguageByCountry: [:], values: nil, removingValues: false))
}
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
@ -78,6 +79,40 @@ final class SecureIdAuthController: ViewController {
self.challengeDisposable.set((twoStepAuthData(account.network)
|> deliverOnMainQueue).start(next: { [weak self] data in
if let strongSelf = self {
let storedPassword = strongSelf.account.telegramApplicationContext.getStoredSecureIdPassword()
if data.currentPasswordDerivation != nil, let storedPassword = storedPassword {
strongSelf.authenthicateDisposable.set((accessSecureId(network: strongSelf.account.network, password: storedPassword)
|> deliverOnMainQueue).start(next: { context in
guard let strongSelf = self, strongSelf.state.verificationState == nil else {
return
}
strongSelf.updateState(animated: true, { state in
var state = state
state.verificationState = .verified(context.context)
switch state {
case var .form(form):
form.formData = form.encryptedFormData.flatMap({ decryptedSecureIdForm(context: context.context, form: $0.form) })
state = .form(form)
case var .list(list):
list.values = list.encryptedValues.flatMap({ decryptedAllSecureIdValues(context: context.context, encryptedValues: $0) })
state = .list(list)
}
return state
})
}, error: { [weak self] error in
guard let strongSelf = self else {
return
}
if strongSelf.state.verificationState == nil {
strongSelf.updateState(animated: true, { state in
var state = state
state.verificationState = .passwordChallenge(hint: data.currentHint ?? "", state: .none, hasRecoveryEmail: data.hasRecovery)
return state
})
}
}))
} else {
strongSelf.updateState { state in
var state = state
if data.currentPasswordDerivation != nil {
@ -88,6 +123,7 @@ final class SecureIdAuthController: ViewController {
return state
}
}
}
}))
switch self.mode {
@ -123,6 +159,7 @@ final class SecureIdAuthController: ViewController {
if let strongSelf = self {
let errorText = strongSelf.presentationData.strings.Login_UnknownError
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
strongSelf.dismiss()
}
})
case .list:
@ -154,6 +191,7 @@ final class SecureIdAuthController: ViewController {
deinit {
self.challengeDisposable.dispose()
self.authenthicateDisposable.dispose()
self.formDisposable?.dispose()
self.deleteDisposable.dispose()
self.recoveryDisposable.dispose()
@ -249,10 +287,16 @@ final class SecureIdAuthController: ViewController {
if let verificationState = self.state.verificationState, case .passwordChallenge(_, .checking, _) = verificationState {
previousHadProgress = true
}
if self.state.removingValues {
previousHadProgress = true
}
var updatedHasProgress = false
if let verificationState = state.verificationState, case .passwordChallenge(_, .checking, _) = verificationState {
updatedHasProgress = true
}
if state.removingValues {
updatedHasProgress = true
}
self.state = state
if self.isNodeLoaded {
@ -292,6 +336,7 @@ final class SecureIdAuthController: ViewController {
guard let strongSelf = self, let verificationState = strongSelf.state.verificationState, case .passwordChallenge(_, .checking, _) = verificationState else {
return
}
strongSelf.account.telegramApplicationContext.storeSecureIdPassword(password: password)
strongSelf.updateState(animated: !inBackground, { state in
var state = state
state.verificationState = .verified(context.context)
@ -341,9 +386,15 @@ final class SecureIdAuthController: ViewController {
}
private func openPasswordHelp() {
guard let verificationState = self.state.verificationState, case let .passwordChallenge(passwordChallenge) = verificationState, case .none = passwordChallenge.state else {
guard let verificationState = self.state.verificationState, case let .passwordChallenge(passwordChallenge) = verificationState else {
return
}
switch passwordChallenge.state {
case .checking:
return
case .none, .invalid:
break
}
if passwordChallenge.hasRecoveryEmail {
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: self.presentationData.theme), title: self.presentationData.strings.Passport_ForgottenPassword, text: self.presentationData.strings.Passport_PasswordReset, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_ResetAccountProtected_Reset, action: { [weak self] in
@ -427,7 +478,9 @@ final class SecureIdAuthController: ViewController {
switch self.state {
case let .form(form):
if case let .form(reqForm) = self.mode, let encryptedFormData = form.encryptedFormData, let formData = form.formData {
let _ = (grantSecureIdAccess(network: self.account.network, peerId: encryptedFormData.servicePeer.id, publicKey: reqForm.publicKey, scope: reqForm.scope, opaquePayload: reqForm.opaquePayload, opaqueNonce: reqForm.opaqueNonce, values: formData.values, requestedFields: formData.requestedFields)
let values = parseRequestedFormFields(formData.requestedFields, values: formData.values).map({ $0.1 }).flatMap({ $0 })
let _ = (grantSecureIdAccess(network: self.account.network, peerId: encryptedFormData.servicePeer.id, publicKey: reqForm.publicKey, scope: reqForm.scope, opaquePayload: reqForm.opaquePayload, opaqueNonce: reqForm.opaqueNonce, values: values, requestedFields: formData.requestedFields)
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.dismiss()
})

View File

@ -1,4 +1,5 @@
import Foundation
import SwiftSignalKit
import Display
import AsyncDisplayKit
import Postbox
@ -10,8 +11,11 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
private let requestLayout: (ContainedViewLayoutTransition) -> Void
private let interaction: SecureIdAuthControllerInteraction
private var hapticFeedback: HapticFeedback?
private var validLayout: (ContainerViewLayout, CGFloat)?
private let activityIndicator: ActivityIndicator
private let scrollNode: ASScrollNode
private let headerNode: SecureIdAuthHeaderNode
private var contentNode: (ASDisplayNode & SecureIdAuthContentNode)?
@ -23,25 +27,56 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
private var state: SecureIdAuthControllerState?
private let deleteValueDisposable = MetaDisposable()
init(account: Account, presentationData: PresentationData, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, interaction: SecureIdAuthControllerInteraction) {
self.account = account
self.presentationData = presentationData
self.requestLayout = requestLayout
self.interaction = interaction
self.activityIndicator = ActivityIndicator(type: .custom(presentationData.theme.list.freeMonoIcon, 40.0, 2.0))
self.activityIndicator.isHidden = true
self.scrollNode = ASScrollNode()
self.headerNode = SecureIdAuthHeaderNode(account: account, theme: presentationData.theme, strings: presentationData.strings)
self.acceptNode = SecureIdAuthAcceptNode(title: presentationData.strings.Passport_Authorize, theme: presentationData.theme)
super.init()
self.addSubnode(self.activityIndicator)
self.scrollNode.view.alwaysBounceVertical = true
self.addSubnode(self.scrollNode)
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.acceptNode.pressed = { [weak self] in
self?.interaction.grant()
guard let strongSelf = self, let state = strongSelf.state, case let .form(form) = state, let formData = form.formData else {
return
}
for (field, _, filled) in parseRequestedFormFields(formData.requestedFields, values: formData.values) {
if !filled {
if let contentNode = strongSelf.contentNode as? SecureIdAuthFormContentNode {
if let rect = contentNode.frameForField(field) {
strongSelf.scrollNode.view.scrollRectToVisible(rect, animated: true)
}
contentNode.highlightField(field)
}
if strongSelf.hapticFeedback == nil {
strongSelf.hapticFeedback = HapticFeedback()
}
strongSelf.hapticFeedback?.error()
return
}
}
strongSelf.interaction.grant()
}
}
deinit {
self.deleteValueDisposable.dispose()
}
func animateIn() {
@ -49,14 +84,26 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
}
func animateOut(completion: (() -> Void)? = nil) {
self.isDisappearing = true
self.view.endEditing(true)
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in
completion?()
})
}
private var isDisappearing = false
private var previousHeaderNodeAlpha: CGFloat = 0.0
private var hadContentNode = false
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
if self.isDisappearing {
return
}
let previousHadContentNode = self.hadContentNode
self.hadContentNode = self.contentNode != nil
var insetOptions: ContainerViewLayoutInsetOptions = []
if self.contentNode is SecureIdAuthPasswordOptionContentNode {
@ -66,19 +113,31 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
var insets = layout.insets(options: insetOptions)
insets.bottom = max(insets.bottom, layout.safeInsets.bottom)
let headerNodeTransition: ContainedViewLayoutTransition = headerNode.bounds.isEmpty ? .immediate : transition
let headerHeight: CGFloat
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - 40.0) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - 40.0) / 2.0)), size: CGSize(width: 40.0, height: 40.0)))
var headerNodeTransition: ContainedViewLayoutTransition = self.headerNode.bounds.height.isZero ? .immediate : transition
if self.previousHeaderNodeAlpha.isZero && !self.headerNode.alpha.isZero {
headerNodeTransition = .immediate
}
self.previousHeaderNodeAlpha = self.headerNode.alpha
let headerLayout: (compact: CGFloat, expanded: CGFloat, apply: (Bool) -> Void)
if self.headerNode.alpha.isZero {
headerHeight = 0.0
headerLayout = (0.0, 0.0, { _ in })
} else {
headerHeight = self.headerNode.updateLayout(width: layout.size.width, transition: headerNodeTransition)
headerLayout = self.headerNode.updateLayout(width: layout.size.width, transition: headerNodeTransition)
}
let acceptHeight = self.acceptNode.updateLayout(width: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
var footerHeight: CGFloat = 0.0
var contentSpacing: CGFloat
transition.updateFrame(node: self.acceptNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - acceptHeight), size: CGSize(width: layout.size.width, height: acceptHeight)))
var acceptNodeTransition = transition
if !previousHadContentNode {
acceptNodeTransition = .immediate
}
acceptNodeTransition.updateFrame(node: self.acceptNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - acceptHeight), size: CGSize(width: layout.size.width, height: acceptHeight)))
if self.acceptNode.supernode != nil {
footerHeight += acceptHeight
contentSpacing = 25.0
@ -104,6 +163,17 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
let contentNodeTransition: ContainedViewLayoutTransition = contentFirstTime ? .immediate : transition
let contentLayout = contentNode.updateLayout(width: layout.size.width, transition: contentNodeTransition)
let headerHeight: CGFloat
if self.contentNode is SecureIdAuthPasswordOptionContentNode && headerLayout.expanded + contentLayout.height + 10.0 + 14.0 + 16.0 > contentRect.height {
headerHeight = headerLayout.compact
headerLayout.apply(false)
} else {
headerHeight = headerLayout.expanded
headerLayout.apply(true)
}
contentSpacing = max(10.0, min(contentSpacing, contentRect.height - (headerHeight + contentLayout.height + 10.0 - 14.0 - 16.0)))
let boundingHeight = headerHeight + contentLayout.height + contentSpacing
var boundingRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: boundingHeight))
@ -126,7 +196,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
contentNode.didAppear()
if transition.isAnimated {
contentNode.animateIn()
if !(contentNode is SecureIdAuthPasswordOptionContentNode || contentNode is SecureIdAuthPasswordSetupContentNode) {
if !(contentNode is SecureIdAuthPasswordOptionContentNode || contentNode is SecureIdAuthPasswordSetupContentNode) && previousHadContentNode {
transition.animatePositionAdditive(node: contentNode, offset: CGPoint(x: layout.size.width, y: 0.0))
}
}
@ -169,6 +239,8 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
func updateState(_ state: SecureIdAuthControllerState, transition: ContainedViewLayoutTransition) {
self.state = state
var displayActivity = false
switch state {
case let .form(form):
if let encryptedFormData = form.encryptedFormData, let verificationState = form.verificationState {
@ -243,6 +315,8 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
if self.contentNode !== contentNode {
self.transitionToContentNode(contentNode, transition: transition)
}
} else {
displayActivity = true
}
case let .list(list):
if let _ = list.encryptedValues, let verificationState = list.verificationState {
@ -268,13 +342,9 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
contentNode = current
} else {
let current = SecureIdAuthPasswordOptionContentNode(theme: presentationData.theme, strings: presentationData.strings, hint: hint, checkPassword: { [weak self] password in
if let strongSelf = self {
strongSelf.interaction.checkPassword(password)
}
self?.interaction.checkPassword(password)
}, passwordHelp: { [weak self] in
if let strongSelf = self {
}
self?.interaction.openPasswordHelp()
})
current.updateIsChecking(challengeState == .checking)
contentNode = current
@ -301,8 +371,13 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
if self.contentNode !== contentNode {
self.transitionToContentNode(contentNode, transition: transition)
}
} else {
displayActivity = true
}
}
if displayActivity != !self.activityIndicator.isHidden {
self.activityIndicator.isHidden = !displayActivity
}
}
private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) {
@ -336,7 +411,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
return !touchedKeys.contains(value.value.key)
}
values.append(contentsOf: updatedValues)
return .form(SecureIdAuthControllerFormState(encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState))
return .form(SecureIdAuthControllerFormState(encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState, removingValues: form.removingValues))
}
}
@ -347,7 +422,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
switch document {
case let .just(type):
if let value = findValue(formData.values, key: type.valueKey)?.1 {
switch value {
switch value.value {
case .passport:
hasValueType = .passport
case .internalPassport:
@ -363,7 +438,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
case let .oneOf(types):
for type in types {
if let value = findValue(formData.values, key: type.valueKey)?.1 {
switch value {
switch value.value {
case .passport:
hasValueType = .passport
case .internalPassport:
@ -401,7 +476,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
switch document {
case let .just(type):
if let value = findValue(formData.values, key: type.valueKey)?.1 {
switch value {
switch value.value {
case .rentalAgreement:
hasValueType = .rentalAgreement
case .bankStatement:
@ -420,7 +495,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
case let .oneOf(types):
for type in types {
if let value = findValue(formData.values, key: type.valueKey)?.1 {
switch value {
switch value.value {
case .rentalAgreement:
hasValueType = .rentalAgreement
case .bankStatement:
@ -489,20 +564,26 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
}
private func presentPlaintextSelection(type: SecureIdPlaintextFormType) {
guard let state = self.state, case let .form(form) = state, let verificationState = form.verificationState, case let .verified(context) = verificationState else {
guard let state = self.state, case let .form(form) = state, let formData = form.formData, let verificationState = form.verificationState, case let .verified(context) = verificationState else {
return
}
var immediatelyAvailableValue: SecureIdValue?
var currentValue: SecureIdValueWithContext?
switch type {
case .phone:
if let peer = form.encryptedFormData?.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone))
}
default:
break
currentValue = findValue(formData.values, key: .phone)?.1
case .email:
currentValue = findValue(formData.values, key: .email)?.1
}
self.interaction.present(SecureIdPlaintextFormController(account: self.account, context: context, type: type, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { [weak self] valueWithContext in
let openForm: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.interaction.present(SecureIdPlaintextFormController(account: strongSelf.account, context: context, type: type, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { valueWithContext in
if let strongSelf = self {
strongSelf.interaction.updateState { state in
if case let .form(form) = state, let formData = form.formData {
@ -520,7 +601,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
if let valueWithContext = valueWithContext {
values.append(valueWithContext)
}
return .form(SecureIdAuthControllerFormState(encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState))
return .form(SecureIdAuthControllerFormState(encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState, removingValues: form.removingValues))
}
return state
}
@ -528,6 +609,57 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
}), nil)
}
if let currentValue = currentValue {
let controller = ActionSheetController(presentationTheme: self.presentationData.theme)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
let text: String
switch currentValue.value {
case .phone:
text = self.presentationData.strings.Passport_Phone_Delete
default:
text = self.presentationData.strings.Passport_Email_Delete
}
controller.setItemGroups([
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: text, color: .destructive, action: { [weak self] in
dismissAction()
guard let strongSelf = self else {
return
}
strongSelf.interaction.updateState { state in
if case var .form(form) = state {
form.removingValues = true
return .form(form)
}
return state
}
strongSelf.deleteValueDisposable.set((deleteSecureIdValues(network: strongSelf.account.network, keys: Set([currentValue.value.key]))
|> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else {
return
}
strongSelf.interaction.updateState { state in
if case var .form(form) = state, let formData = form.formData {
form.removingValues = false
form.formData = SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: formData.values.filter {
$0.value.key != currentValue.value.key
})
return .form(form)
}
return state
}
}))
})]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.view.endEditing(true)
self.interaction.present(controller, nil)
} else {
openForm()
}
}
private func openListField(_ field: SecureIdAuthListContentField) {
guard let state = self.state, case let .list(list) = state, let verificationState = list.verificationState, case let .verified(context) = verificationState else {
return

View File

@ -25,6 +25,7 @@ struct SecureIdAuthControllerFormState: Equatable {
var encryptedFormData: SecureIdEncryptedFormData?
var formData: SecureIdForm?
var verificationState: SecureIdAuthControllerVerificationState?
var removingValues: Bool = false
static func ==(lhs: SecureIdAuthControllerFormState, rhs: SecureIdAuthControllerFormState) -> Bool {
if (lhs.formData != nil) != (rhs.formData != nil) {
@ -47,6 +48,10 @@ struct SecureIdAuthControllerFormState: Equatable {
return false
}
if lhs.removingValues != rhs.removingValues {
return false
}
return true
}
}
@ -56,6 +61,7 @@ struct SecureIdAuthControllerListState: Equatable {
var encryptedValues: EncryptedAllSecureIdValues?
var primaryLanguageByCountry: [String: String]?
var values: [SecureIdValueWithContext]?
var removingValues: Bool = false
static func ==(lhs: SecureIdAuthControllerListState, rhs: SecureIdAuthControllerListState) -> Bool {
if lhs.verificationState != rhs.verificationState {
@ -70,6 +76,9 @@ struct SecureIdAuthControllerListState: Equatable {
if lhs.values != rhs.values {
return false
}
if lhs.removingValues != rhs.removingValues {
return false
}
return true
}
}
@ -97,4 +106,13 @@ enum SecureIdAuthControllerState: Equatable {
}
}
}
var removingValues: Bool {
switch self {
case let .form(form):
return form.removingValues
case let .list(list):
return list.removingValues
}
}
}

View File

@ -9,6 +9,7 @@ private let passwordFont = Font.regular(16.0)
private let buttonFont = Font.regular(17.0)
final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode, UITextFieldDelegate {
private let requestedFields: [SecureIdRequestedFormField]
private let fieldBackgroundNode: ASDisplayNode
private let fieldNodes: [SecureIdAuthFormFieldNode]
private let headerNode: ImmediateTextNode
@ -17,14 +18,15 @@ final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode,
private var validLayout: CGFloat?
init(theme: PresentationTheme, strings: PresentationStrings, peer: Peer, privacyPolicyUrl: String?, form: SecureIdForm, openField: @escaping (SecureIdParsedRequestedFormField) -> Void, openURL: @escaping (String) -> Void, openMention: @escaping (TelegramPeerMention) -> Void) {
self.requestedFields = form.requestedFields
self.fieldBackgroundNode = ASDisplayNode()
self.fieldBackgroundNode.isLayerBacked = true
self.fieldBackgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
var fieldNodes: [SecureIdAuthFormFieldNode] = []
for field in parseRequestedFormFields(form.requestedFields) {
fieldNodes.append(SecureIdAuthFormFieldNode(theme: theme, strings: strings, field: field, values: form.values, selected: {
for (field, fieldValues, _) in parseRequestedFormFields(self.requestedFields, values: form.values) {
fieldNodes.append(SecureIdAuthFormFieldNode(theme: theme, strings: strings, field: field, values: fieldValues, selected: {
openField(field)
}))
}
@ -82,8 +84,12 @@ final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode,
}
func updateValues(_ values: [SecureIdValueWithContext]) {
for fieldNode in self.fieldNodes {
fieldNode.updateValues(values)
var index = 0
for (_, fieldValues, _) in parseRequestedFormFields(self.requestedFields, values: values) {
if index < self.fieldNodes.count {
self.fieldNodes[index].updateValues(fieldValues)
}
index += 1
}
}
@ -134,5 +140,22 @@ final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode,
func willDisappear() {
}
func frameForField(_ field: SecureIdParsedRequestedFormField) -> CGRect? {
for fieldNode in self.fieldNodes {
if fieldNode.field == field {
return fieldNode.frame
}
}
return nil
}
func highlightField(_ field: SecureIdParsedRequestedFormField) {
for fieldNode in self.fieldNodes {
if fieldNode.field == field {
fieldNode.highlight()
}
}
}
}

View File

@ -1,4 +1,5 @@
import Foundation
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramCore
@ -46,23 +47,23 @@ enum SecureIdRequestedAddressDocument: Int32 {
}
}
struct ParsedRequestedPersonalDetails {
struct ParsedRequestedPersonalDetails: Equatable {
var nativeNames: Bool
}
enum SecureIdParsedRequestedFormField {
enum SecureIdParsedRequestedFormField: Equatable {
case identity(personalDetails: ParsedRequestedPersonalDetails?, document: ParsedRequestedIdentityDocument?, selfie: Bool, translation: Bool)
case address(addressDetails: Bool, document: ParsedRequestedAddressDocument?, translation: Bool)
case phone
case email
}
enum ParsedRequestedIdentityDocument {
enum ParsedRequestedIdentityDocument: Equatable {
case just(SecureIdRequestedIdentityDocument)
case oneOf(Set<SecureIdRequestedIdentityDocument>)
}
enum ParsedRequestedAddressDocument {
enum ParsedRequestedAddressDocument: Equatable {
case just(SecureIdRequestedAddressDocument)
case oneOf(Set<SecureIdRequestedAddressDocument>)
}
@ -109,14 +110,14 @@ private struct RequestedFieldValues {
}
}
func parseRequestedFormFields(_ types: [SecureIdRequestedFormField]) -> [SecureIdParsedRequestedFormField] {
var values = RequestedFieldValues()
func parseRequestedFormFields(_ types: [SecureIdRequestedFormField], values: [SecureIdValueWithContext]) -> [(SecureIdParsedRequestedFormField, [SecureIdValueWithContext], Bool)] {
var requestedValues = RequestedFieldValues()
for type in types {
switch type {
case let .just(value):
let subResult = parseRequestedFieldValues(type: value)
values.merge(subResult)
requestedValues.merge(subResult)
case let .oneOf(subTypes):
var oneOfResult = RequestedFieldValues()
var oneOfIdentity = Set<SecureIdRequestedIdentityDocument>()
@ -145,37 +146,165 @@ func parseRequestedFormFields(_ types: [SecureIdRequestedFormField]) -> [SecureI
if !oneOfAddress.isEmpty {
oneOfResult.address.documents.append(.oneOf(oneOfAddress))
}
values.merge(oneOfResult)
requestedValues.merge(oneOfResult)
}
}
var result: [SecureIdParsedRequestedFormField] = []
if values.identity.details || !values.identity.documents.isEmpty {
if values.identity.documents.isEmpty {
result.append(.identity(personalDetails: ParsedRequestedPersonalDetails(nativeNames: values.identity.nativeNames), document: nil, selfie: false, translation: false))
if requestedValues.identity.details || !requestedValues.identity.documents.isEmpty {
if requestedValues.identity.documents.isEmpty {
result.append(.identity(personalDetails: ParsedRequestedPersonalDetails(nativeNames: requestedValues.identity.nativeNames), document: nil, selfie: false, translation: false))
} else {
for document in values.identity.documents {
result.append(.identity(personalDetails: values.identity.details ? ParsedRequestedPersonalDetails(nativeNames: values.identity.nativeNames) : nil, document: document, selfie: values.identity.selfie, translation: values.identity.translation))
for document in requestedValues.identity.documents {
result.append(.identity(personalDetails: requestedValues.identity.details ? ParsedRequestedPersonalDetails(nativeNames: requestedValues.identity.nativeNames) : nil, document: document, selfie: requestedValues.identity.selfie, translation: requestedValues.identity.translation))
}
}
}
if values.address.details || !values.address.documents.isEmpty {
if values.address.documents.isEmpty {
if requestedValues.address.details || !requestedValues.address.documents.isEmpty {
if requestedValues.address.documents.isEmpty {
result.append(.address(addressDetails: true, document: nil, translation: false))
} else {
for document in values.address.documents {
result.append(.address(addressDetails: values.address.details, document: document, translation: values.address.translation))
for document in requestedValues.address.documents {
result.append(.address(addressDetails: requestedValues.address.details, document: document, translation: requestedValues.address.translation))
}
}
}
if values.phone {
if requestedValues.phone {
result.append(.phone)
}
if values.email {
if requestedValues.email {
result.append(.email)
}
return result
return result.map { field in
let (fieldValues, filled) = findValuesForField(field: field, values: values)
return (field, fieldValues, filled)
}
}
private func findValuesForField(field: SecureIdParsedRequestedFormField, values: [SecureIdValueWithContext]) -> ([SecureIdValueWithContext], Bool) {
switch field {
case let .identity(personalDetails, document, selfie, translation):
var filled = true
var result: [SecureIdValueWithContext] = []
if personalDetails != nil {
if let value = findValue(values, key: .personalDetails)?.1 {
result.append(value)
} else {
filled = false
}
}
if let document = document {
switch document {
case let .just(type):
if let value = findValue(values, key: type.valueKey)?.1 {
result.append(value)
let data = extractValueAdditionalData(value.value)
if selfie && !data.selfie {
filled = false
}
if translation && !data.translation {
filled = false
}
} else {
filled = false
}
case let .oneOf(types):
var anyDocument = false
var bestMatchingValue: SecureIdValueWithContext?
inner: for type in types {
if let value = findValue(values, key: type.valueKey)?.1 {
if bestMatchingValue == nil {
bestMatchingValue = value
}
let data = extractValueAdditionalData(value.value)
var dataFilled = true
if selfie && !data.selfie {
dataFilled = false
}
if translation && !data.translation {
dataFilled = false
}
if dataFilled {
bestMatchingValue = value
anyDocument = true
break inner
}
}
}
if !anyDocument {
filled = false
}
if let bestMatchingValue = bestMatchingValue {
result.append(bestMatchingValue)
}
}
}
return (result, filled)
case let .address(addressDetails, document, translation):
var filled = true
var result: [SecureIdValueWithContext] = []
if addressDetails {
if let value = findValue(values, key: .address)?.1 {
result.append(value)
} else {
filled = false
}
}
if let document = document {
switch document {
case let .just(type):
if let value = findValue(values, key: type.valueKey)?.1 {
result.append(value)
let data = extractValueAdditionalData(value.value)
if translation && !data.translation {
filled = false
}
} else {
filled = false
}
case let .oneOf(types):
var anyDocument = false
var bestMatchingValue: SecureIdValueWithContext?
inner: for type in types {
if let value = findValue(values, key: type.valueKey)?.1 {
if bestMatchingValue == nil {
bestMatchingValue = value
}
let data = extractValueAdditionalData(value.value)
var dataFilled = true
if translation && !data.translation {
dataFilled = false
}
if dataFilled {
bestMatchingValue = value
anyDocument = true
break inner
}
}
}
if !anyDocument {
filled = false
}
if let bestMatchingValue = bestMatchingValue {
result.append(bestMatchingValue)
}
}
}
return (result, filled)
case .phone:
if let value = findValue(values, key: .phone)?.1 {
return ([value], true)
} else {
return ([], false)
}
case .email:
if let value = findValue(values, key: .email)?.1 {
return ([value], true)
} else {
return ([], false)
}
}
}
private func parseRequestedFieldValues(type: SecureIdRequestedFormFieldValue) -> RequestedFieldValues {
@ -268,7 +397,7 @@ private func fieldTitleAndText(field: SecureIdParsedRequestedFormField, strings:
}
if personalDetails != nil {
if let value = findValue(values, key: .personalDetails), case let .personalDetails(personalDetailsValue) = value.1 {
if let value = findValue(values, key: .personalDetails), case let .personalDetails(personalDetailsValue) = value.1.value {
if !text.isEmpty {
text.append(", ")
}
@ -291,7 +420,7 @@ private func fieldTitleAndText(field: SecureIdParsedRequestedFormField, strings:
}
if addressDetails {
if let value = findValue(values, key: .address), case let .address(addressValue) = value.1 {
if let value = findValue(values, key: .address), case let .address(addressValue) = value.1.value {
if !text.isEmpty {
text.append(", ")
}
@ -302,7 +431,7 @@ private func fieldTitleAndText(field: SecureIdParsedRequestedFormField, strings:
title = strings.Passport_FieldPhone
placeholder = strings.Passport_FieldPhoneHelp
if let value = findValue(values, key: .phone), case let .phone(phoneValue) = value.1 {
if let value = findValue(values, key: .phone), case let .phone(phoneValue) = value.1.value {
if !text.isEmpty {
text.append(", ")
}
@ -312,7 +441,7 @@ private func fieldTitleAndText(field: SecureIdParsedRequestedFormField, strings:
title = strings.Passport_FieldEmail
placeholder = strings.Passport_FieldEmailHelp
if let value = findValue(values, key: .email), case let .email(emailValue) = value.1 {
if let value = findValue(values, key: .email), case let .email(emailValue) = value.1.value {
if !text.isEmpty {
text.append(", ")
}
@ -373,7 +502,7 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode {
private var validLayout: (CGFloat, Bool, Bool)?
private let field: SecureIdParsedRequestedFormField
let field: SecureIdParsedRequestedFormField
private let theme: PresentationTheme
private let strings: PresentationStrings
@ -475,7 +604,7 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode {
switch document {
case let .just(type):
if let value = findValue(values, key: type.valueKey)?.1 {
let data = extractValueAdditionalData(value)
let data = extractValueAdditionalData(value.value)
if selfie && !data.selfie {
filled = false
}
@ -489,7 +618,7 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode {
var anyDocument = false
for type in types {
if let value = findValue(values, key: type.valueKey)?.1 {
let data = extractValueAdditionalData(value)
let data = extractValueAdditionalData(value.value)
var dataFilled = true
if selfie && !data.selfie {
dataFilled = false
@ -517,7 +646,7 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode {
switch document {
case let .just(type):
if let value = findValue(values, key: type.valueKey)?.1 {
let data = extractValueAdditionalData(value)
let data = extractValueAdditionalData(value.value)
if translation && !data.translation {
filled = false
}
@ -528,7 +657,7 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode {
var anyDocument = false
for type in types {
if let value = findValue(values, key: type.valueKey)?.1 {
let data = extractValueAdditionalData(value)
let data = extractValueAdditionalData(value.value)
var dataFilled = true
if translation && !data.translation {
dataFilled = false
@ -602,4 +731,15 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode {
@objc private func buttonPressed() {
self.selected()
}
func highlight() {
self.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
self.highlightedBackgroundNode.alpha = 1.0
self.view.superview?.bringSubview(toFront: self.view)
Queue.mainQueue().after(1.0, {
self.highlightedBackgroundNode.alpha = 0.0
self.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
})
}
}

View File

@ -64,45 +64,58 @@ final class SecureIdAuthHeaderNode: ASDisplayNode {
self.verificationState = verificationState
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> (compact: CGFloat, expanded: CGFloat, apply: (Bool) -> Void) {
if !self.iconNode.isHidden {
guard let image = self.iconNode.image else {
return 1.0
return (1.0, 1.0, { _ in
})
}
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0), y: 0.0), size: image.size)
let resultHeight: CGFloat = image.size.height
return resultHeight
return (image.size.height, image.size.height, { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0), y: 0.0), size: image.size)
})
} else {
let avatarSize = CGSize(width: 70.0, height: 70.0)
let serviceAvatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize.width) / 2.0), y: 0.0), size: avatarSize)
transition.updateFrame(node: self.serviceAvatarNode, frame: serviceAvatarFrame)
if let verificationState = self.verificationState, case .noChallenge = verificationState {
transition.updateAlpha(node: self.serviceAvatarNode, alpha: 0.0)
} else {
transition.updateAlpha(node: self.serviceAvatarNode, alpha: 1.0)
}
let avatarTitleSpacing: CGFloat = 20.0
let titleSize = self.titleNode.updateLayout(CGSize(width: width - 20.0, height: 1000.0))
var expandedHeight: CGFloat = titleSize.height
if !self.serviceAvatarNode.isHidden {
expandedHeight += avatarSize.height + avatarTitleSpacing
}
let compactHeight = titleSize.height
return (compactHeight, expandedHeight, { [weak self] expanded in
guard let strongSelf = self else {
return
}
transition.updateAlpha(node: strongSelf.serviceAvatarNode, alpha: expanded ? 1.0 : 0.0)
var titleOffset: CGFloat = 0.0
if !self.serviceAvatarNode.alpha.isZero {
if expanded && !strongSelf.serviceAvatarNode.isHidden && !strongSelf.serviceAvatarNode.alpha.isZero {
titleOffset = avatarSize.height + avatarTitleSpacing
}
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: titleOffset), size: titleSize)
ContainedViewLayoutTransition.immediate.updateFrame(node: self.titleNode, frame: titleFrame)
let previousTitleFrame = strongSelf.titleNode.frame
ContainedViewLayoutTransition.immediate.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
transition.animatePositionAdditive(node: strongSelf.titleNode, offset: CGPoint(x: 0.0, y: previousTitleFrame.midY - titleFrame.midY))
var resultHeight: CGFloat = titleSize.height
if !self.serviceAvatarNode.alpha.isZero {
resultHeight += avatarSize.height + avatarTitleSpacing
let serviceAvatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize.width) / 2.0), y: titleFrame.minY - avatarTitleSpacing - avatarSize.height), size: avatarSize)
transition.updateFrame(node: strongSelf.serviceAvatarNode, frame: serviceAvatarFrame)
if let verificationState = strongSelf.verificationState, case .noChallenge = verificationState {
strongSelf.serviceAvatarNode.isHidden = true
} else {
strongSelf.serviceAvatarNode.isHidden = false
}
return resultHeight
})
}
}
}

View File

@ -87,7 +87,7 @@ private func fieldTitleAndText(field: SecureIdAuthListContentField, strings: Pre
title = strings.Passport_FieldPhone
placeholder = strings.Passport_FieldPhoneHelp
if let value = findValue(values, key: .phone), case let .phone(phoneValue) = value.1 {
if let value = findValue(values, key: .phone), case let .phone(phoneValue) = value.1.value {
if !text.isEmpty {
text.append(", ")
}
@ -97,7 +97,7 @@ private func fieldTitleAndText(field: SecureIdAuthListContentField, strings: Pre
title = strings.Passport_FieldEmail
placeholder = strings.Passport_FieldEmailHelp
if let value = findValue(values, key: .email), case let .email(emailValue) = value.1 {
if let value = findValue(values, key: .email), case let .email(emailValue) = value.1.value {
if !text.isEmpty {
text.append(", ")
}

View File

@ -2347,7 +2347,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode<SecureIdDocum
}
if let backSideDocument = innerState.backSideDocument, backSideDocument.resource.isEqual(to: resource) {
innerState.selfieDocument = nil
innerState.backSideDocument = nil
}
for i in 0 ..< innerState.documents.count {

View File

@ -424,7 +424,7 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present
return entries
}
func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacySettingsKind, current: SelectivePrivacySettings, callSettings: VoiceCallSettings? = nil, callIntegrationAvailable: Bool? = nil, updated: @escaping (SelectivePrivacySettings, VoiceCallSettings?) -> Void) -> ViewController {
func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacySettingsKind, current: SelectivePrivacySettings, callSettings: VoiceCallSettings? = nil, voipConfiguration: VoipConfiguration? = nil, callIntegrationAvailable: Bool? = nil, updated: @escaping (SelectivePrivacySettings, VoiceCallSettings?) -> Void) -> ViewController {
let strings = account.telegramApplicationContext.currentPresentationData.with { $0 }.strings
var initialEnableFor = Set<PeerId>()
@ -438,7 +438,7 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy
case let .enableEveryone(disableFor):
initialDisableFor = disableFor
}
let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, saving: false, callDataSaving: callSettings?.dataSaving, callP2PMode: callSettings?.p2pMode, callIntegrationAvailable: callIntegrationAvailable, callIntegrationEnabled: callSettings?.enableSystemIntegration)
let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, saving: false, callDataSaving: callSettings?.dataSaving, callP2PMode: callSettings?.p2pMode ?? voipConfiguration?.defaultP2PMode, callIntegrationAvailable: callIntegrationAvailable, callIntegrationEnabled: callSettings?.enableSystemIntegration)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)

View File

@ -95,6 +95,8 @@ public final class TelegramApplicationContext {
}
private var experimentalUISettingsDisposable: Disposable?
private var storedPassword: (String, CFAbsoluteTime, SwiftSignalKit.Timer)?
public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, account: Account?, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, postbox: Postbox) {
self.mediaManager = MediaManager(postbox: postbox, inForeground: applicationBindings.applicationInForeground)
@ -210,6 +212,27 @@ public final class TelegramApplicationContext {
public func attachOverlayMediaController(_ controller: OverlayMediaController) {
self.mediaManager.overlayMediaManager.attachOverlayMediaController(controller)
}
public func storeSecureIdPassword(password: String) {
self.storedPassword?.2.invalidate()
let timer = SwiftSignalKit.Timer(timeout: 1.0 * 60.0 * 60.0, repeat: false, completion: { [weak self] in
self?.storedPassword = nil
}, queue: Queue.mainQueue())
self.storedPassword = (password, CFAbsoluteTimeGetCurrent(), timer)
timer.start()
}
public func getStoredSecureIdPassword() -> String? {
if let (password, timestamp, timer) = self.storedPassword {
if CFAbsoluteTimeGetCurrent() > timestamp + 1.0 * 60.0 * 60.0 {
timer.invalidate()
self.storedPassword = nil
}
return password
} else {
return nil
}
}
}
public extension Account {

View File

@ -562,6 +562,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep
})
var initialFocusImpl: (() -> Void)?
var didAppear = false
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), dataPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue
|> map { presentationData, state, data -> (ItemListControllerState, (ItemListNodeState<TwoStepVerificationUnlockSettingsEntry>, TwoStepVerificationUnlockSettingsEntry.ItemGenerationArguments)) in
@ -596,10 +597,11 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: TwoStepVerificationUnlockSettingsEntryTag.password, emptyStateItem: emptyStateItem, animateChanges: false)
let listState = ItemListNodeState(entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: didAppear ? TwoStepVerificationUnlockSettingsEntryTag.password : nil, emptyStateItem: emptyStateItem, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
}
|> afterDisposed {
actionsDisposable.dispose()
}
@ -632,6 +634,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep
if !firstTime {
return
}
didAppear = true
initialFocusImpl?()
}

View File

@ -1,5 +1,6 @@
import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
public enum VoiceCallDataSaving: Int32 {
@ -8,22 +9,16 @@ public enum VoiceCallDataSaving: Int32 {
case always
}
public enum VoiceCallP2PMode: Int32 {
case never = 0
case contacts = 1
case always = 2
}
public struct VoiceCallSettings: PreferencesEntry, Equatable {
public var dataSaving: VoiceCallDataSaving
public var p2pMode: VoiceCallP2PMode
public var p2pMode: VoiceCallP2PMode?
public var enableSystemIntegration: Bool
public static var defaultSettings: VoiceCallSettings {
return VoiceCallSettings(dataSaving: .never, p2pMode: .contacts, enableSystemIntegration: true)
return VoiceCallSettings(dataSaving: .never, p2pMode: nil, enableSystemIntegration: true)
}
init(dataSaving: VoiceCallDataSaving, p2pMode: VoiceCallP2PMode, enableSystemIntegration: Bool) {
init(dataSaving: VoiceCallDataSaving, p2pMode: VoiceCallP2PMode?, enableSystemIntegration: Bool) {
self.dataSaving = dataSaving
self.p2pMode = p2pMode
self.enableSystemIntegration = enableSystemIntegration
@ -31,13 +26,21 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable {
public init(decoder: PostboxDecoder) {
self.dataSaving = VoiceCallDataSaving(rawValue: decoder.decodeInt32ForKey("ds", orElse: 0))!
self.p2pMode = VoiceCallP2PMode(rawValue: decoder.decodeInt32ForKey("p2pMode", orElse: 1))!
if let value = decoder.decodeOptionalInt32ForKey("p2pMode") {
self.p2pMode = VoiceCallP2PMode(rawValue: value) ?? .contacts
} else {
self.p2pMode = nil
}
self.enableSystemIntegration = decoder.decodeInt32ForKey("enableSystemIntegration", orElse: 1) != 0
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.dataSaving.rawValue, forKey: "ds")
encoder.encodeInt32(self.p2pMode.rawValue, forKey: "p2pMode")
if let p2pMode = self.p2pMode {
encoder.encodeInt32(p2pMode.rawValue, forKey: "p2pMode")
} else {
encoder.encodeNil(forKey: "p2pMode")
}
encoder.encodeInt32(self.enableSystemIntegration ? 1 : 0, forKey: "enableSystemIntegration")
}