diff --git a/TelegramUI/CallKitIntergation.swift b/TelegramUI/CallKitIntergation.swift index 0c39209d60..862799af12 100644 --- a/TelegramUI/CallKitIntergation.swift +++ b/TelegramUI/CallKitIntergation.swift @@ -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 { diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index f67674d0db..025de14125 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -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 } diff --git a/TelegramUI/CheckDeviceAccess.swift b/TelegramUI/CheckDeviceAccess.swift index c016ea3046..0a744cef44 100644 --- a/TelegramUI/CheckDeviceAccess.swift +++ b/TelegramUI/CheckDeviceAccess.swift @@ -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) + } } }) } diff --git a/TelegramUI/DeclareEncodables.swift b/TelegramUI/DeclareEncodables.swift index 256be99d4a..1e2a82914d 100644 --- a/TelegramUI/DeclareEncodables.swift +++ b/TelegramUI/DeclareEncodables.swift @@ -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) }) diff --git a/TelegramUI/FindSecureIdValue.swift b/TelegramUI/FindSecureIdValue.swift index a6e9d7107f..fbda63988a 100644 --- a/TelegramUI/FindSecureIdValue.swift +++ b/TelegramUI/FindSecureIdValue.swift @@ -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 diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index 28e160d987..5e1d69e6b4 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -419,8 +419,9 @@ class ItemListController: ViewController { } } - self.didAppear?(!self.didAppearOnce) + let firstTime = !self.didAppearOnce self.didAppearOnce = true + self.didAppear?(firstTime) } override func viewWillDisappear(_ animated: Bool) { diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index fd89e3ec67..ccfe947db5 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -331,19 +331,23 @@ class ItemListControllerNode: 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 + } } } } diff --git a/TelegramUI/MediaPlayerAudioRenderer.swift b/TelegramUI/MediaPlayerAudioRenderer.swift index b2f1d327c4..163c3fbae8 100644 --- a/TelegramUI/MediaPlayerAudioRenderer.swift +++ b/TelegramUI/MediaPlayerAudioRenderer.swift @@ -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 diff --git a/TelegramUI/PresentationCall.swift b/TelegramUI/PresentationCall.swift index fd1391da89..d1daa293b0 100644 --- a/TelegramUI/PresentationCall.swift +++ b/TelegramUI/PresentationCall.swift @@ -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) { + 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) { 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 { diff --git a/TelegramUI/PresentationCallManager.swift b/TelegramUI/PresentationCallManager.swift index 83fed551e3..03b1251d2e 100644 --- a/TelegramUI/PresentationCallManager.swift +++ b/TelegramUI/PresentationCallManager.swift @@ -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 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, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager) { + public init(postbox: Postbox, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), networkType: Signal, 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 = 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 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 { - 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 = 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 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 in + return .single(true) } - }) - |> mapToSignal { _ -> Signal in - return .single(true) } } } diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index 063b7b5c36..5e7404d69d 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -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 diff --git a/TelegramUI/SecureIdAuthController.swift b/TelegramUI/SecureIdAuthController.swift index 4bf9b788de..8ee45e38e6 100644 --- a/TelegramUI/SecureIdAuthController.swift +++ b/TelegramUI/SecureIdAuthController.swift @@ -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() }) diff --git a/TelegramUI/SecureIdAuthControllerNode.swift b/TelegramUI/SecureIdAuthControllerNode.swift index b6e4eb862d..d10562f912 100644 --- a/TelegramUI/SecureIdAuthControllerNode.swift +++ b/TelegramUI/SecureIdAuthControllerNode.swift @@ -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) { diff --git a/TelegramUI/SecureIdAuthControllerState.swift b/TelegramUI/SecureIdAuthControllerState.swift index a7a22438ee..42337e7e4d 100644 --- a/TelegramUI/SecureIdAuthControllerState.swift +++ b/TelegramUI/SecureIdAuthControllerState.swift @@ -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 + } + } } diff --git a/TelegramUI/SecureIdAuthFormContentNode.swift b/TelegramUI/SecureIdAuthFormContentNode.swift index b426f1f08e..8a6af47a46 100644 --- a/TelegramUI/SecureIdAuthFormContentNode.swift +++ b/TelegramUI/SecureIdAuthFormContentNode.swift @@ -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() + } + } + } } diff --git a/TelegramUI/SecureIdAuthFormFieldNode.swift b/TelegramUI/SecureIdAuthFormFieldNode.swift index f68da9d44c..2f36583a5f 100644 --- a/TelegramUI/SecureIdAuthFormFieldNode.swift +++ b/TelegramUI/SecureIdAuthFormFieldNode.swift @@ -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) } -enum ParsedRequestedAddressDocument { +enum ParsedRequestedAddressDocument: Equatable { case just(SecureIdRequestedAddressDocument) case oneOf(Set) } @@ -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() @@ -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) + }) + } } diff --git a/TelegramUI/SecureIdAuthHeaderNode.swift b/TelegramUI/SecureIdAuthHeaderNode.swift index 5b4ec17260..1521d9b6d4 100644 --- a/TelegramUI/SecureIdAuthHeaderNode.swift +++ b/TelegramUI/SecureIdAuthHeaderNode.swift @@ -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 + } + }) } } } diff --git a/TelegramUI/SecureIdAuthListFieldNode.swift b/TelegramUI/SecureIdAuthListFieldNode.swift index dc0717cc4f..358faa07e8 100644 --- a/TelegramUI/SecureIdAuthListFieldNode.swift +++ b/TelegramUI/SecureIdAuthListFieldNode.swift @@ -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(", ") } diff --git a/TelegramUI/SecureIdDocumentFormControllerNode.swift b/TelegramUI/SecureIdDocumentFormControllerNode.swift index c9394d28f0..d2dcb5b704 100644 --- a/TelegramUI/SecureIdDocumentFormControllerNode.swift +++ b/TelegramUI/SecureIdDocumentFormControllerNode.swift @@ -2347,7 +2347,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode 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() @@ -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) diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 39ecbdae9c..23c8657db1 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -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 { diff --git a/TelegramUI/TwoStepVerificationUnlockController.swift b/TelegramUI/TwoStepVerificationUnlockController.swift index aae0b58a6a..d19968709f 100644 --- a/TelegramUI/TwoStepVerificationUnlockController.swift +++ b/TelegramUI/TwoStepVerificationUnlockController.swift @@ -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.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.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?() } diff --git a/TelegramUI/VoiceCallSettings.swift b/TelegramUI/VoiceCallSettings.swift index 95d105a6a2..18badf993b 100644 --- a/TelegramUI/VoiceCallSettings.swift +++ b/TelegramUI/VoiceCallSettings.swift @@ -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") }