mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-16 03:09:56 +00:00
no message
This commit is contained in:
parent
3a414eeea2
commit
0648840a26
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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) })
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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,40 +105,47 @@ 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
|
||||
}
|
||||
|> take(1)
|
||||
|> 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,34 +265,58 @@ 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
|
||||
return peerView.peerIsContact
|
||||
}))
|
||||
|> deliverOnMainQueue
|
||||
|> beforeNext { [weak self] internalId, currentNetworkType, isContact in
|
||||
if let strongSelf = self {
|
||||
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)
|
||||
strongSelf.currentCall = call
|
||||
strongSelf.currentCallPromise.set(.single(call))
|
||||
strongSelf.hasActiveCallsPromise.set(true)
|
||||
strongSelf.removeCurrentCallDisposable.set((call.canBeRemoved
|
||||
|> deliverOnMainQueue).start(next: { [weak call] value in
|
||||
if value, let strongSelf = self, let call = call {
|
||||
if strongSelf.currentCall === call {
|
||||
strongSelf.currentCall = nil
|
||||
strongSelf.currentCallPromise.set(.single(nil))
|
||||
strongSelf.hasActiveCallsPromise.set(false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
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 { 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?.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)
|
||||
strongSelf.removeCurrentCallDisposable.set((call.canBeRemoved
|
||||
|> deliverOnMainQueue).start(next: { [weak call] value in
|
||||
if value, let strongSelf = self, let call = call {
|
||||
if strongSelf.currentCall === call {
|
||||
strongSelf.currentCall = nil
|
||||
strongSelf.currentCallPromise.set(.single(nil))
|
||||
strongSelf.hasActiveCallsPromise.set(false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
|> mapToSignal { value -> Signal<Bool, NoError> in
|
||||
return .single(true)
|
||||
}
|
||||
})
|
||||
|> mapToSignal { _ -> Signal<Bool, NoError> in
|
||||
return .single(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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])
|
||||
|> take(1)
|
||||
|> map { view -> VoiceCallSettings in
|
||||
let voiceCallSettings: VoiceCallSettings
|
||||
if let value = view.values[ApplicationSpecificPreferencesKeys.voiceCallSettings] as? VoiceCallSettings {
|
||||
voiceCallSettings = value
|
||||
} else {
|
||||
voiceCallSettings = VoiceCallSettings.defaultSettings
|
||||
}
|
||||
|
||||
return voiceCallSettings
|
||||
let callsSignal = account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.voiceCallSettings, PreferencesKeys.voipConfiguration])
|
||||
|> take(1)
|
||||
|> 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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -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,14 +79,49 @@ final class SecureIdAuthController: ViewController {
|
||||
self.challengeDisposable.set((twoStepAuthData(account.network)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateState { state in
|
||||
var state = state
|
||||
if data.currentPasswordDerivation != nil {
|
||||
state.verificationState = .passwordChallenge(hint: data.currentHint ?? "", state: .none, hasRecoveryEmail: data.hasRecovery)
|
||||
} else {
|
||||
state.verificationState = .noChallenge(data.unconfirmedEmailPattern)
|
||||
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 {
|
||||
state.verificationState = .passwordChallenge(hint: data.currentHint ?? "", state: .none, hasRecoveryEmail: data.hasRecovery)
|
||||
} else {
|
||||
state.verificationState = .noChallenge(data.unconfirmedEmailPattern)
|
||||
}
|
||||
return state
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
}))
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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,40 +27,83 @@ 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() {
|
||||
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}, passwordHelp: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
|
||||
}
|
||||
self?.interaction.checkPassword(password)
|
||||
}, passwordHelp: { [weak self] in
|
||||
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,43 +564,100 @@ 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
|
||||
if let strongSelf = self {
|
||||
strongSelf.interaction.updateState { state in
|
||||
if case let .form(form) = state, let formData = form.formData {
|
||||
var values = formData.values
|
||||
switch type {
|
||||
case .phone:
|
||||
while let index = findValue(values, key: .phone)?.0 {
|
||||
values.remove(at: index)
|
||||
}
|
||||
case .email:
|
||||
while let index = findValue(values, key: .email)?.0 {
|
||||
values.remove(at: index)
|
||||
}
|
||||
}
|
||||
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 state
|
||||
}
|
||||
let openForm: () -> Void = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
}), nil)
|
||||
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 {
|
||||
var values = formData.values
|
||||
switch type {
|
||||
case .phone:
|
||||
while let index = findValue(values, key: .phone)?.0 {
|
||||
values.remove(at: index)
|
||||
}
|
||||
case .email:
|
||||
while let index = findValue(values, key: .email)?.0 {
|
||||
values.remove(at: index)
|
||||
}
|
||||
}
|
||||
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, removingValues: form.removingValues))
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
}), 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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 titleOffset: CGFloat = 0.0
|
||||
if !self.serviceAvatarNode.alpha.isZero {
|
||||
titleOffset = avatarSize.height + avatarTitleSpacing
|
||||
var expandedHeight: CGFloat = titleSize.height
|
||||
if !self.serviceAvatarNode.isHidden {
|
||||
expandedHeight += avatarSize.height + avatarTitleSpacing
|
||||
}
|
||||
let compactHeight = titleSize.height
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: titleOffset), size: titleSize)
|
||||
ContainedViewLayoutTransition.immediate.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
var resultHeight: CGFloat = titleSize.height
|
||||
if !self.serviceAvatarNode.alpha.isZero {
|
||||
resultHeight += avatarSize.height + avatarTitleSpacing
|
||||
}
|
||||
return resultHeight
|
||||
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 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)
|
||||
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))
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(", ")
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -562,44 +562,46 @@ 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
|
||||
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
let title: String
|
||||
switch data {
|
||||
case let .access(configuration):
|
||||
title = presentationData.strings.TwoStepAuth_Title
|
||||
if let configuration = configuration {
|
||||
if state.checking {
|
||||
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
|
||||
} else {
|
||||
switch configuration {
|
||||
case .notSet:
|
||||
break
|
||||
case let .set(_, _, _, hasSecureValues):
|
||||
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: true, action: {
|
||||
arguments.checkPassword()
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
|
||||
}
|
||||
case .manage:
|
||||
title = presentationData.strings.PrivacySettings_TwoStepAuth
|
||||
|> map { presentationData, state, data -> (ItemListControllerState, (ItemListNodeState<TwoStepVerificationUnlockSettingsEntry>, TwoStepVerificationUnlockSettingsEntry.ItemGenerationArguments)) in
|
||||
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
let title: String
|
||||
switch data {
|
||||
case let .access(configuration):
|
||||
title = presentationData.strings.TwoStepAuth_Title
|
||||
if let configuration = configuration {
|
||||
if state.checking {
|
||||
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
|
||||
} else {
|
||||
switch configuration {
|
||||
case .notSet:
|
||||
break
|
||||
case let .set(_, _, _, hasSecureValues):
|
||||
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: true, action: {
|
||||
arguments.checkPassword()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
} else {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
|
||||
}
|
||||
case .manage:
|
||||
title = presentationData.strings.PrivacySettings_TwoStepAuth
|
||||
if state.checking {
|
||||
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
|
||||
}
|
||||
}
|
||||
|
||||
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: didAppear ? TwoStepVerificationUnlockSettingsEntryTag.password : nil, emptyStateItem: emptyStateItem, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
@ -632,6 +634,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep
|
||||
if !firstTime {
|
||||
return
|
||||
}
|
||||
didAppear = true
|
||||
initialFocusImpl?()
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user