diff --git a/Images.xcassets/Secure ID/EmptyPasswordIcon.imageset/Contents.json b/Images.xcassets/Secure ID/EmptyPasswordIcon.imageset/Contents.json new file mode 100644 index 0000000000..3dd535cbed --- /dev/null +++ b/Images.xcassets/Secure ID/EmptyPasswordIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PasswordPlaceholderIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PasswordPlaceholderIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Secure ID/EmptyPasswordIcon.imageset/PasswordPlaceholderIcon@2x.png b/Images.xcassets/Secure ID/EmptyPasswordIcon.imageset/PasswordPlaceholderIcon@2x.png new file mode 100644 index 0000000000..e47006e63e Binary files /dev/null and b/Images.xcassets/Secure ID/EmptyPasswordIcon.imageset/PasswordPlaceholderIcon@2x.png differ diff --git a/Images.xcassets/Secure ID/EmptyPasswordIcon.imageset/PasswordPlaceholderIcon@3x.png b/Images.xcassets/Secure ID/EmptyPasswordIcon.imageset/PasswordPlaceholderIcon@3x.png new file mode 100644 index 0000000000..393a9de85e Binary files /dev/null and b/Images.xcassets/Secure ID/EmptyPasswordIcon.imageset/PasswordPlaceholderIcon@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index fb129c38ac..3a7e30f962 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -327,6 +327,8 @@ D0CB27CF20C17A4A001ACF93 /* TermsOfServiceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CB27CE20C17A4A001ACF93 /* TermsOfServiceController.swift */; }; D0CB27D220C17A7F001ACF93 /* TermsOfServiceControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CB27D120C17A7F001ACF93 /* TermsOfServiceControllerNode.swift */; }; D0CE67941F7DB45100FFB557 /* ChatMessageContactBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE67931F7DB45100FFB557 /* ChatMessageContactBubbleContentNode.swift */; }; + D0CE6F6E213EDF8800BCD44B /* SecureIdAuthPasswordSetupContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE6F6D213EDF8800BCD44B /* SecureIdAuthPasswordSetupContentNode.swift */; }; + D0CE6F70213EEE5000BCD44B /* CreatePasswordController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE6F6F213EEE5000BCD44B /* CreatePasswordController.swift */; }; D0CE8CE51F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */; }; D0CE8CE71F6F35A300AA2DB0 /* ChatTextInputPanelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE61F6F35A300AA2DB0 /* ChatTextInputPanelState.swift */; }; D0CE8CEC1F6FCCA300AA2DB0 /* TransformImageArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CEB1F6FCCA300AA2DB0 /* TransformImageArguments.swift */; }; @@ -1644,6 +1646,8 @@ D0CB27D120C17A7F001ACF93 /* TermsOfServiceControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceControllerNode.swift; sourceTree = ""; }; D0CE1BD21E51BC6100404327 /* DebugController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugController.swift; sourceTree = ""; }; D0CE67931F7DB45100FFB557 /* ChatMessageContactBubbleContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageContactBubbleContentNode.swift; sourceTree = ""; }; + D0CE6F6D213EDF8800BCD44B /* SecureIdAuthPasswordSetupContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdAuthPasswordSetupContentNode.swift; sourceTree = ""; }; + D0CE6F6F213EEE5000BCD44B /* CreatePasswordController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePasswordController.swift; sourceTree = ""; }; D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputAccessoryItem.swift; sourceTree = ""; }; D0CE8CE61F6F35A300AA2DB0 /* ChatTextInputPanelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputPanelState.swift; sourceTree = ""; }; D0CE8CEB1F6FCCA300AA2DB0 /* TransformImageArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformImageArguments.swift; sourceTree = ""; }; @@ -3118,6 +3122,7 @@ D0BE30482061C0F500FBE6D8 /* SecureIdAuthHeaderNode.swift */, D0147BA6206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift */, D0BE30462061C0BC00FBE6D8 /* SecureIdAuthPasswordOptionContentNode.swift */, + D0CE6F6D213EDF8800BCD44B /* SecureIdAuthPasswordSetupContentNode.swift */, D093D7DA2062CFF500BC3599 /* SecureIdAuthFormContentNode.swift */, D093D7DC2062D09A00BC3599 /* SecureIdAuthFormFieldNode.swift */, D08D7E78209FA2930005D80C /* SecureIdValues.swift */, @@ -4210,6 +4215,7 @@ D0FA0ABE1E76E17F005BB9B7 /* TwoStepVerificationPasswordEntryController.swift */, D0FA0AC01E7725AA005BB9B7 /* TwoStepVerificationResetController.swift */, D0760B231E9D015D00F1F3C4 /* PasscodeOptionsController.swift */, + D0CE6F6F213EEE5000BCD44B /* CreatePasswordController.swift */, ); name = "Privacy and Security"; sourceTree = ""; @@ -4827,6 +4833,7 @@ D0CE8CEC1F6FCCA300AA2DB0 /* TransformImageArguments.swift in Sources */, D0EC6D661EB9F58800EBF1C3 /* ContactsSectionHeaderAccessoryItem.swift in Sources */, D0EC6D671EB9F58800EBF1C3 /* ContactListNameIndexHeader.swift in Sources */, + D0CE6F6E213EDF8800BCD44B /* SecureIdAuthPasswordSetupContentNode.swift in Sources */, D07E413D208A494D00FCA8F0 /* ProxyServerActionSheetController.swift in Sources */, D0EC6D681EB9F58800EBF1C3 /* AuthorizationSequenceController.swift in Sources */, D0EC6D691EB9F58800EBF1C3 /* AuthorizationSequenceSplashController.swift in Sources */, @@ -5204,6 +5211,7 @@ D0EC6E3C1EB9F58900EBF1C3 /* ItemListPeerActionItem.swift in Sources */, D0EC6E3D1EB9F58900EBF1C3 /* ItemListMultilineInputItem.swift in Sources */, D0CE8CE71F6F35A300AA2DB0 /* ChatTextInputPanelState.swift in Sources */, + D0CE6F70213EEE5000BCD44B /* CreatePasswordController.swift in Sources */, D0EC6E3E1EB9F58900EBF1C3 /* ItemListSectionHeaderItem.swift in Sources */, D0EC6E3F1EB9F58900EBF1C3 /* ItemListTextItem.swift in Sources */, D0EC6E401EB9F58900EBF1C3 /* ItemListActivityTextItem.swift in Sources */, @@ -5777,6 +5785,115 @@ }; name = "Debug AppStore LLC"; }; + D0CE6F02213DC32300BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = ""; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = "Release AppStore LLC"; + }; + D0CE6F03213DC32300BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + COPY_PHASE_STRIP = YES; + DEVELOPMENT_TEAM = X834Q8SBVP; + HEADERMAP_USES_VFS = YES; + INFOPLIST_FILE = TelegramUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies -driver-show-incremental"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + }; + name = "Release AppStore LLC"; + }; + D0CE6F04213DC32300BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEVELOPMENT_TEAM = X834Q8SBVP; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADERMAP_USES_VFS = YES; + HEADER_SEARCH_PATHS = "third-party/ogg"; + INFOPLIST_FILE = "$(SRCROOT)/TelegramUI/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/third-party/opus/lib", + "$(PROJECT_DIR)/third-party/libwebp/lib", + "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", + ); + OTHER_CFLAGS = ( + "-DTGVOIP_USE_CUSTOM_CRYPTO", + "-DWEBRTC_APM_DEBUG_DUMP=0", + "-DWEBRTC_POSIX", + "-DMINIMAL_ASDK=1", + ); + OTHER_LDFLAGS = "-ObjC"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; + PRODUCT_NAME = TelegramUI; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 4.0; + USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/third-party/FFmpeg-iOS/include"; + }; + name = "Release AppStore LLC"; + }; D0EC6E9E1EB9F79800EBF1C3 /* Debug Hockeyapp */ = { isa = XCBuildConfiguration; buildSettings = { @@ -6054,6 +6171,7 @@ D0EC6E9F1EB9F79800EBF1C3 /* Release Hockeyapp */, D0924FF01FE52C29003F693F /* Release Hockeyapp Internal */, D0EC6EA01EB9F79800EBF1C3 /* Release AppStore */, + D0CE6F04213DC32300BCD44B /* Release AppStore LLC */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = "Release Hockeyapp"; @@ -6067,6 +6185,7 @@ D0FC40921D5B8E7500261D9D /* Release Hockeyapp */, D0924FEE1FE52C29003F693F /* Release Hockeyapp Internal */, D0400EDB1D5B900A007931CE /* Release AppStore */, + D0CE6F02213DC32300BCD44B /* Release AppStore LLC */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = "Release Hockeyapp"; @@ -6080,6 +6199,7 @@ D0FC40981D5B8E7500261D9D /* Release Hockeyapp */, D0924FEF1FE52C29003F693F /* Release Hockeyapp Internal */, D0400EDD1D5B900A007931CE /* Release AppStore */, + D0CE6F03213DC32300BCD44B /* Release AppStore LLC */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = "Release Hockeyapp"; diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 63bb8e77c9..5c451621ab 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -210,173 +210,193 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.ready.set(.never()) self.scrollToTop = { [weak self] in - if let strongSelf = self, strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.scrollToTop() + guard let strongSelf = self, strongSelf.isNodeLoaded else { + return } + strongSelf.chatDisplayNode.scrollToTop() } self.attemptNavigation = { [weak self] action in - if let strongSelf = self { - if let _ = strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_DiscardVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - if let strongSelf = self { - strongSelf.stopMediaRecorder() - } - action() - })]), in: .window(.root)) - - return false - } + guard let strongSelf = self else { + return true + } + if let _ = strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_DiscardVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + self?.stopMediaRecorder() + action() + })]), in: .window(.root)) + + return false } return true } let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in - if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { - for media in message.media { - if let action = media as? TelegramMediaAction { - switch action.action { - case .pinnedMessageUpdated: - for attribute in message.attributes { - if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) - break - } + guard let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) else { + return false + } + for media in message.media { + if let action = media as? TelegramMediaAction { + switch action.action { + case .pinnedMessageUpdated: + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) + break } - default: - break + } + default: + break + } + return true + } + } + + return openChatMessage(account: account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { + self?.chatDisplayNode.dismissInput() + }, present: { c, a in + self?.present(c, in: .window(.root), with: a) + }, transitionNode: { messageId, media in + var selectedNode: (ASDisplayNode, () -> UIView?)? + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: messageId, media: media) { + selectedNode = result + } } - return true } } - - return openChatMessage(account: account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { - self?.chatDisplayNode.dismissInput() - }, present: { c, a in - self?.present(c, in: .window(.root), with: a) - }, transitionNode: { messageId, media in - var selectedNode: (ASDisplayNode, () -> UIView?)? - if let strongSelf = self { - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - if let result = itemNode.transitionNode(id: messageId, media: media) { - selectedNode = result + return selectedNode + }, addToTransitionSurface: { view in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) + }, openUrl: { url in + self?.openUrl(url, concealed: false) + }, openPeer: { peer, navigation in + self?.openPeer(peerId: peer.id, navigation: navigation, fromMessage: nil) + }, callPeer: { peerId in + self?.controllerInteraction?.callPeer(peerId) + }, enqueueMessage: { message in + self?.sendMessages([message]) + }, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference in + self?.controllerInteraction?.sendSticker(fileReference) + } : nil, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in + if let strongSelf = self { + strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal + |> deliverOnMainQueue).start(next: { entry in + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + var messageIdAndMedia: [MessageId: [Media]] = [:] + + if let entry = entry, entry.index == centralIndex { + messageIdAndMedia[message.id] = [galleryMedia] + } + + controllerInteraction.hiddenMedia = messageIdAndMedia + + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateHiddenMedia() } } } - } - return selectedNode - }, addToTransitionSurface: { view in - if let strongSelf = self { - strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) - } - }, openUrl: { url in - self?.openUrl(url, concealed: false) - }, openPeer: { peer, navigation in - self?.openPeer(peerId: peer.id, navigation: navigation, fromMessage: nil) - }, callPeer: { peerId in - self?.controllerInteraction?.callPeer(peerId) - }, enqueueMessage: { message in - self?.sendMessages([message]) - }, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference in - self?.controllerInteraction?.sendSticker(fileReference) - } : nil, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in - if let strongSelf = self { - strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { entry in - if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - var messageIdAndMedia: [MessageId: [Media]] = [:] - - if let entry = entry, entry.index == centralIndex { - messageIdAndMedia[message.id] = [galleryMedia] - } - - controllerInteraction.hiddenMedia = messageIdAndMedia - - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - itemNode.updateHiddenMedia() - } - } - } - })) - } - }) - } - return false + })) + } + }) }, openPeer: { [weak self] id, navigation, fromMessage in - if let strongSelf = self { - strongSelf.openPeer(peerId: id, navigation: navigation, fromMessage: fromMessage) - } + self?.openPeer(peerId: id, navigation: navigation, fromMessage: fromMessage) }, openPeerMention: { [weak self] name in - if let strongSelf = self { - strongSelf.openPeerMention(name) - } + self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, node, frame in - if let strongSelf = self, strongSelf.isNodeLoaded { - if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { - var updatedMessages = messages - for i in 0 ..< updatedMessages.count { - if updatedMessages[i].id == message.id { - let message = updatedMessages.remove(at: i) - updatedMessages.insert(message, at: 0) + guard let strongSelf = self, strongSelf.isNodeLoaded else { + return + } + if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, account: strongSelf.account, messages: updatedMessages, interfaceInteraction: strongSelf.interfaceInteraction, debugStreamSingleVideo: { id in + self?.debugStreamSingleVideo(id) + }).start(next: { actions in + guard let strongSelf = self, !actions.isEmpty else { + return + } + var contextMenuController: ContextMenuController? + var contextActions: [ContextMenuAction] = [] + var sheetActions: [ChatMessageContextMenuSheetAction] = [] + for action in actions { + switch action { + case let .context(contextAction): + contextActions.append(contextAction) + case let .sheet(sheetAction): + sheetActions.append(sheetAction) + } + } + + if !contextActions.isEmpty { + contextMenuController = ContextMenuController(actions: contextActions, catchTapsOutside: true) + } + + contextMenuController?.dismissed = { + if let strongSelf = self { + strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: nil, sheetActions: nil, displayContextMenuController: nil) + } + } + + var hasActions = false + for media in updatedMessages[0].media { + if media is TelegramMediaAction { + hasActions = true break } } - let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, account: strongSelf.account, messages: updatedMessages, interfaceInteraction: strongSelf.interfaceInteraction, debugStreamSingleVideo: { id in - self?.debugStreamSingleVideo(id) - }).start(next: { actions in - if let strongSelf = self, !actions.isEmpty { - var contextMenuController: ContextMenuController? - var contextActions: [ContextMenuAction] = [] - var sheetActions: [ChatMessageContextMenuSheetAction] = [] - for action in actions { - switch action { - case let .context(contextAction): - contextActions.append(contextAction) - case let .sheet(sheetAction): - sheetActions.append(sheetAction) + if hasActions { + if let contextMenuController = contextMenuController { + strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { + guard let strongSelf = self else { + return nil } - } - - if !contextActions.isEmpty { - contextMenuController = ContextMenuController(actions: contextActions, catchTapsOutside: true) - } - - contextMenuController?.dismissed = { - if let strongSelf = self { - strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: nil, sheetActions: nil, displayContextMenuController: nil) - } - } - - strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: updatedMessages[0].stableId, sheetActions: sheetActions, displayContextMenuController: contextMenuController.flatMap { ($0, node, frame) }) + return (node, frame, strongSelf.displayNode, strongSelf.displayNode.bounds) + })) } - }) - } + } else { + strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: updatedMessages[0].stableId, sheetActions: sheetActions, displayContextMenuController: contextMenuController.flatMap { ($0, node, frame) }) + } + }) } }, navigateToMessage: { [weak self] fromId, id in self?.navigateToMessage(from: fromId, to: .id(id)) }, clickThroughMessage: { [weak self] in self?.chatDisplayNode.dismissInput() }, toggleMessagesSelection: { [weak self] ids, value in - if let strongSelf = self, strongSelf.isNodeLoaded { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessages(ids, value: value) } }) + guard let strongSelf = self, strongSelf.isNodeLoaded else { + return } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessages(ids, value: value) } }) }, sendMessage: { [weak self] text in - if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) - } - }) - var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text, enabledTypes: .all) - if !entities.isEmpty { - attributes.append(TextEntitiesMessageAttribute(entities: entities)) - } - strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + guard let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) else { + return } + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(text, enabledTypes: .all) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) }, sendSticker: { [weak self] fileReference in if let strongSelf = self { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ @@ -815,17 +835,14 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } return false }, navigateToFirstDateMessage: { [weak self] timestamp in - guard let `self` = self else {return} - switch self.chatLocation { - case let .peer(peerId): - self.messageIndexDisposable.set((searchMessageIdByTimestamp(account: self.account, peerId: peerId, timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT())) |> deliverOnMainQueue).start(next: { [weak self] messageId in - guard let `self` = self else {return} - if let messageId = messageId { - self.navigateToMessage(from: nil, to: .id(messageId), scrollPosition: .bottom(0)) - } - })) - default: - break + guard let strongSelf = self else { + return + } + switch strongSelf.chatLocation { + case let .peer(peerId): + strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil) + default: + break } }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { @@ -1757,12 +1774,25 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }, deleteMessages: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { let messageIds = Set(messages.map { $0.id }) - strongSelf.messageContextDisposable.set((chatAvailableMessageActions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds) |> deliverOnMainQueue).start(next: { actions in + strongSelf.messageContextDisposable.set((chatAvailableMessageActions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds) + |> deliverOnMainQueue).start(next: { actions in if let strongSelf = self, !actions.options.isEmpty { if let banAuthor = actions.banAuthor { strongSelf.presentBanMessageOptions(author: banAuthor, messageIds: messageIds, options: actions.options) } else { - strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options) + var isAction = false + if messages.count == 1 { + for media in messages[0].media { + if media is TelegramMediaAction { + isAction = true + } + } + } + if isAction && (actions.options == .deleteGlobally || actions.options == .deleteLocally) { + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).start() + } else { + strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options) + } } } })) @@ -3090,6 +3120,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) legacyController.statusBar.statusBarStyle = .Ignore + legacyController.controllerLoaded = { [weak legacyController] in + legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true + } let emptyController = LegacyEmptyController(context: legacyController.context)! let navigationController = makeLegacyNavigationController(rootController: emptyController) @@ -3222,37 +3255,39 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings } - |> deliverOnMainQueue).start(next: { [weak self] settings in + |> deliverOnMainQueue).start(next: { [weak self] settings in + guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return + } + let _ = legacyAssetPicker(applicationContext: strongSelf.account.telegramApplicationContext, presentationData: strongSelf.presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true).start(next: { generator in if let strongSelf = self { - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - let _ = legacyAssetPicker(applicationContext: strongSelf.account.telegramApplicationContext, presentationData: strongSelf.presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true).start(next: { generator in - if let strongSelf = self { - let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) - legacyController.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBar.style.style - let controller = generator(legacyController.context) - legacyController.bind(controller: controller) - legacyController.deferScreenEdgeGestures = [.top] - - configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer) - controller.descriptionGenerator = legacyAssetPickerItemGenerator() - controller.completionBlock = { [weak legacyController] signals in - if let legacyController = legacyController { - legacyController.dismiss() - completion(signals!) - } - } - controller.dismissalBlock = { [weak legacyController] in - if let legacyController = legacyController { - legacyController.dismiss() - } - } - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(legacyController, in: .window(.root)) - } - }) + let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) + legacyController.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBar.style.style + legacyController.controllerLoaded = { [weak legacyController] in + legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true } + let controller = generator(legacyController.context) + legacyController.bind(controller: controller) + legacyController.deferScreenEdgeGestures = [.top] + + configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer) + controller.descriptionGenerator = legacyAssetPickerItemGenerator() + controller.completionBlock = { [weak legacyController] signals in + if let legacyController = legacyController { + legacyController.dismiss() + completion(signals!) + } + } + controller.dismissalBlock = { [weak legacyController] in + if let legacyController = legacyController { + legacyController.dismiss() + } + } + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(legacyController, in: .window(.root)) } }) + }) } private func presentMapPicker(editingMessage: Bool) { @@ -3803,23 +3838,27 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } let historyView = chatHistoryViewForLocation(.InitialSearch(location: searchLocation, count: 50), account: self.account, chatLocation: self.chatLocation, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView - |> mapToSignal { historyView -> Signal in - switch historyView { - case .Loading: - return .complete() - case let .HistoryView(view, _, _, _, _): - for entry in view.entries { - if case let .MessageEntry(message, _, _, _) = entry { - if message.id == messageLocation.messageId { - return .single(MessageIndex(message)) - } + |> mapToSignal { historyView -> Signal in + switch historyView { + case .Loading: + return .complete() + case let .HistoryView(view, _, _, _, _): + for entry in view.entries { + if case let .MessageEntry(message, _, _, _) = entry { + if message.id == messageLocation.messageId { + return .single(MessageIndex(message)) } } - return .single(nil) - } + } + if case let .index(index) = searchLocation { + return .single(index) + } + return .single(nil) } - |> take(1) - self.messageIndexDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] index in + } + |> take(1) + self.messageIndexDisposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] index in if let strongSelf = self, let index = index { strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index, animated: animated, scrollPosition: scrollPosition) completion?() @@ -4634,7 +4673,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return UIDropProposal(operation: .cancel) } - let dropLocation = session.location(in: self.chatDisplayNode.view) + //let dropLocation = session.location(in: self.chatDisplayNode.view) self.chatDisplayNode.updateDropInteraction(isActive: true) let operation: UIDropOperation @@ -4644,12 +4683,15 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin @available(iOSApplicationExtension 11.0, *) public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { - session.loadObjects(ofClass: UIImage.self) { imageItems in + session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in + guard let strongSelf = self else { + return + } let images = imageItems as! [UIImage] - self.chatDisplayNode.updateDropInteraction(isActive: false) + strongSelf.chatDisplayNode.updateDropInteraction(isActive: false) - self.chatDisplayNode.displayPasteMenu(images) + strongSelf.chatDisplayNode.displayPasteMenu(images) } } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 28e931a8ea..e3a311a573 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -67,7 +67,7 @@ public final class ChatControllerInteraction { let openSearch: () -> Void let setupReply: (MessageId) -> Void let canSetupReply: (Message) -> Bool - let navigateToFirstDateMessage:(Int32)->Void + let navigateToFirstDateMessage: (Int32) -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 46677ee8f7..b48ea5e4cd 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -12,7 +12,6 @@ private struct MessageContextMenuData { let canPin: Bool let canEdit: Bool let canSelect: Bool - let canContextDelete: Bool let resourceStatus: MediaResourceStatus? let messageActions: ChatAvailableMessageActions } @@ -199,7 +198,6 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: canDeleteMessage = account.peerId == message.author?.id } - let canContextDelete = isAction && canDeleteMessage if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty { switch chatPresentationInterfaceState.chatLocation { case .peer: @@ -305,7 +303,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } - return MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, canContextDelete: canContextDelete, resourceStatus: resourceStatus, messageActions: messageActions) + return MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions) } return dataSignal |> deliverOnMainQueue |> map { data -> [ChatMessageContextMenuAction] in @@ -424,13 +422,12 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: interfaceInteraction.beginMessageSelection(messages.map { $0.id }) }))) } - if data.canContextDelete { + if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && isAction { actions.append(.context(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete), action: { interfaceInteraction.deleteMessages(messages) }))) } - if data.messageActions.options.contains(.forward) { actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_ContextMenuForward, action: { interfaceInteraction.forwardMessages(messages) @@ -503,6 +500,12 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag optionsMap[id]!.insert(.forward) optionsMap[id]!.insert(.deleteLocally) } else if let peer = transaction.getPeer(id.peerId), let message = transaction.getMessage(id) { + var isAction = false + for media in message.media { + if media is TelegramMediaAction { + isAction = true + } + } if let channel = peer as? TelegramChannel { if message.flags.contains(.Incoming), channel.adminRights == nil, !channel.flags.contains(.isCreator) { optionsMap[id]!.insert(.report) @@ -520,7 +523,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag banPeer = nil } } - if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia { + if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction { optionsMap[id]!.insert(.forward) } if !message.flags.contains(.Incoming) { @@ -532,7 +535,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag } } else if let group = peer as? TelegramGroup { if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia { - optionsMap[id]!.insert(.forward) + if !isAction { + optionsMap[id]!.insert(.forward) + } if message.flags.contains(.Incoming) { optionsMap[id]!.insert(.report) } @@ -549,7 +554,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag } } } else if let _ = peer as? TelegramUser { - if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia { + if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction { optionsMap[id]!.insert(.forward) } optionsMap[id]!.insert(.deleteLocally) @@ -577,14 +582,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag } else { assertionFailure() } - if message.media.first is TelegramMediaAction { - optionsMap[id] = [] - } } else { optionsMap[id]!.insert(.deleteLocally) } - - } if !optionsMap.isEmpty { diff --git a/TelegramUI/ChatMessageDateHeader.swift b/TelegramUI/ChatMessageDateHeader.swift index 19f4315a9a..6ff8ff3379 100644 --- a/TelegramUI/ChatMessageDateHeader.swift +++ b/TelegramUI/ChatMessageDateHeader.swift @@ -168,7 +168,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { override func didLoad() { super.didLoad() - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.view.addGestureRecognizer(ListViewTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } func updateThemeAndStrings(theme: ChatPresentationThemeData, strings: PresentationStrings) { @@ -256,7 +256,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { super.touchesCancelled(touches, with: event) } - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + @objc func tapGesture(_ recognizer: ListViewTapGestureRecognizer) { if case .ended = recognizer.state { action?(self.localTimestamp) } diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index f7bdfc23cd..171c85b748 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -571,6 +571,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let textInputNode = self.textInputNode, textInputNode.keyboardAppearance != keyboardAppearance, textInputNode.isFirstResponder() { textInputNode.resignFirstResponder() + textInputNode.becomeFirstResponder() } self.textInputNode?.keyboardAppearance = keyboardAppearance diff --git a/TelegramUI/CreatePasswordController.swift b/TelegramUI/CreatePasswordController.swift new file mode 100644 index 0000000000..bdb9159d01 --- /dev/null +++ b/TelegramUI/CreatePasswordController.swift @@ -0,0 +1,294 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private enum CreatePasswordField { + case password + case passwordConfirmation + case hint + case email +} + +private final class CreatePasswordControllerArguments { + let updateFieldText: (CreatePasswordField, String) -> Void + + init(updateFieldText: @escaping (CreatePasswordField, String) -> Void) { + self.updateFieldText = updateFieldText + } +} + +private enum CreatePasswordSection: Int32 { + case password + case hint + case email +} + +private enum CreatePasswordEntryTag: ItemListItemTag { + case password + case passwordConfirmation + case hint + case email + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? CreatePasswordEntryTag { + return self == other + } else { + return false + } + } +} + +private enum CreatePasswordEntry: ItemListNodeEntry, Equatable { + case passwordHeader(PresentationTheme, String) + case password(PresentationTheme, String, String) + case passwordConfirmation(PresentationTheme, String, String) + case passwordInfo(PresentationTheme, String) + + case hintHeader(PresentationTheme, String) + case hint(PresentationTheme, String, String) + case hintInfo(PresentationTheme, String) + + case emailHeader(PresentationTheme, String) + case email(PresentationTheme, String, String) + case emailInfo(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .passwordHeader, .password, .passwordConfirmation, .passwordInfo: + return CreatePasswordSection.password.rawValue + case .hintHeader, .hint, .hintInfo: + return CreatePasswordSection.hint.rawValue + case .emailHeader, .email, .emailInfo: + return CreatePasswordSection.email.rawValue + } + } + + var stableId: Int32 { + switch self { + case .passwordHeader: + return 0 + case .password: + return 1 + case .passwordConfirmation: + return 2 + case .passwordInfo: + return 3 + + case .hintHeader: + return 4 + case .hint: + return 5 + case .hintInfo: + return 6 + + case .emailHeader: + return 7 + case .email: + return 8 + case .emailInfo: + return 9 + } + } + + static func <(lhs: CreatePasswordEntry, rhs: CreatePasswordEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: CreatePasswordControllerArguments) -> ListViewItem { + switch self { + case let .passwordHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .password(theme, text, value): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: value, placeholder: text, type: .password, spacing: 0.0, tag: CreatePasswordEntryTag.password, sectionId: self.section, textUpdated: { updatedText in + arguments.updateFieldText(.password, updatedText) + }, action: { + }) + case let .passwordConfirmation(theme, text, value): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: value, placeholder: text, type: .password, spacing: 0.0, tag: CreatePasswordEntryTag.passwordConfirmation, sectionId: self.section, textUpdated: { updatedText in + arguments.updateFieldText(.passwordConfirmation, updatedText) + }, action: { + }) + case let .passwordInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .hintHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .hint(theme, text, value): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: value, placeholder: text, type: .regular(capitalization: true, autocorrection: false), spacing: 0.0, tag: CreatePasswordEntryTag.password, sectionId: self.section, textUpdated: { updatedText in + arguments.updateFieldText(.password, updatedText) + }, action: { + }) + case let .hintInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .emailHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .email(theme, text, value): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: value, placeholder: text, type: .email, spacing: 0.0, tag: CreatePasswordEntryTag.password, sectionId: self.section, textUpdated: { updatedText in + arguments.updateFieldText(.password, updatedText) + }, action: { + }) + case let .emailInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + } + } +} + +private struct CreatePasswordControllerState: Equatable { + var passwordText: String = "" + var passwordConfirmationText: String = "" + var hintText: String = "" + var emailText: String = "" + var saving: Bool = false + var pendingEmail: String? = nil +} + +private func createPasswordControllerEntries(presentationData: PresentationData, state: CreatePasswordControllerState) -> [CreatePasswordEntry] { + var entries: [CreatePasswordEntry] = [] + + entries.append(.passwordHeader(presentationData.theme, presentationData.strings.FastTwoStepSetup_PasswordSection)) + entries.append(.password(presentationData.theme, presentationData.strings.FastTwoStepSetup_PasswordPlaceholder, state.passwordText)) + entries.append(.passwordConfirmation(presentationData.theme, presentationData.strings.FastTwoStepSetup_PasswordConfirmationPlaceholder, state.passwordConfirmationText)) + entries.append(.passwordInfo(presentationData.theme, presentationData.strings.FastTwoStepSetup_PasswordHelp)) + + entries.append(.hintHeader(presentationData.theme, presentationData.strings.FastTwoStepSetup_HintSection)) + entries.append(.hint(presentationData.theme, presentationData.strings.FastTwoStepSetup_HintPlaceholder, state.hintText)) + entries.append(.hintInfo(presentationData.theme, presentationData.strings.FastTwoStepSetup_HintHelp)) + + entries.append(.emailHeader(presentationData.theme, presentationData.strings.FastTwoStepSetup_EmailSection)) + entries.append(.email(presentationData.theme, presentationData.strings.FastTwoStepSetup_EmailPlaceholder, state.emailText)) + entries.append(.emailInfo(presentationData.theme, presentationData.strings.FastTwoStepSetup_EmailHelp)) + + return entries +} + +func createPasswordController(account: Account, completion: @escaping (String, String) -> Void) -> ViewController { + let statePromise = ValuePromise(CreatePasswordControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: CreatePasswordControllerState()) + let updateState: ((CreatePasswordControllerState) -> CreatePasswordControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + + let actionsDisposable = DisposableSet() + + let saveDisposable = MetaDisposable() + actionsDisposable.add(saveDisposable) + + let arguments = CreatePasswordControllerArguments(updateFieldText: { field, updatedText in + updateState { state in + var state = state + switch field { + case .password: + state.passwordText = updatedText + case .passwordConfirmation: + state.passwordConfirmationText = updatedText + case .hint: + state.hintText = updatedText + case .email: + state.emailText = updatedText + } + return state + } + }) + + var initialFocusImpl: (() -> Void)? + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) + |> deliverOnMainQueue + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, CreatePasswordEntry.ItemGenerationArguments)) in + + var rightNavigationButton: ItemListNavigationButton? + if state.saving { + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) + } else { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.passwordText.isEmpty, action: { + var state: CreatePasswordControllerState? + updateState { s in + state = s + return s + } + if let state = state { + if state.passwordText.isEmpty { + } else if state.passwordText != state.passwordConfirmationText { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } else { + let saveImpl: () -> Void = { + updateState { state in + var state = state + state.saving = true + return state + } + saveDisposable.set((updateTwoStepVerificationPassword(network: account.network, currentPassword: nil, updatedPassword: .password(password: state.passwordText, hint: state.hintText, email: state.emailText)) + |> deliverOnMainQueue).start(next: { update in + switch update { + case .none: + break + case let .password(password, pendingEmailPattern): + if let pendingEmailPattern = pendingEmailPattern { + updateState { state in + var state = state + state.saving = false + state.pendingEmail = pendingEmailPattern + return state + } + } else { + completion(password, state.hintText) + } + } + }, error: { _ in + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + })) + } + + if state.emailText.isEmpty { + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: presentationData.strings.TwoStepAuth_EmailSkip, action: { + saveImpl() + })]), nil) + } else { + saveImpl() + } + } + } + }) + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.FastTwoStepSetup_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: createPasswordControllerEntries(presentationData: presentationData, state: state), style: .blocks, focusItemTag: CreatePasswordEntryTag.password, emptyStateItem: nil, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(account: account, state: signal) + presentControllerImpl = { [weak controller] c, p in + if let controller = controller { + controller.present(c, in: .window(.root), with: p) + } + } + initialFocusImpl = { [weak controller] in + guard let controller = controller, controller.didAppearOnce else { + return + } + var resultItemNode: ItemListSingleLineInputItemNode? + let _ = controller.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: CreatePasswordEntryTag.password) { + resultItemNode = itemNode + return true + } + return false + }) + if let resultItemNode = resultItemNode { + resultItemNode.focus() + } + } + controller.didAppear = { + initialFocusImpl?() + } + + return controller +} diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index 09f119444e..7434334914 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -126,6 +126,9 @@ func legacyPasteMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, allow let legacyController = LegacyController(presentation: .custom, theme: theme) legacyController.statusBar.statusBarStyle = .Hide + legacyController.controllerLoaded = { [weak legacyController] in + legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true + } let baseController = TGViewController(context: legacyController.context)! legacyController.bind(controller: baseController) var hasTimer = false diff --git a/TelegramUI/LegacyCamera.swift b/TelegramUI/LegacyCamera.swift index f5894d5f2b..32bcbf8f75 100644 --- a/TelegramUI/LegacyCamera.swift +++ b/TelegramUI/LegacyCamera.swift @@ -45,6 +45,7 @@ func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmen if let controller = controller { cameraView?.detachPreviewView() controller.beginTransitionIn(from: startFrame) + controller.view.disablesInteractiveTransitionGestureRecognizer = true } } diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index 07a7669ba5..9cd028cf20 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -36,6 +36,9 @@ private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyCo if parentController.statusBar.statusBarStyle == .Hide { self.controller?.statusBar.statusBarStyle = parentController.statusBar.statusBarStyle } + if parentController.view.disablesInteractiveTransitionGestureRecognizer { + self.controller?.view.disablesInteractiveTransitionGestureRecognizer = true + } self.controller?.view.frame = parentController.view.bounds } } diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift index 260aaf8e6f..620e5ac75c 100644 --- a/TelegramUI/LegacyInstantVideoController.swift +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -86,6 +86,7 @@ final class InstantVideoController: LegacyController { func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, account: Account, peerId: PeerId, send: @escaping (EnqueueMessage) -> Void) -> InstantVideoController { let legacyController = InstantVideoController(presentation: .custom, theme: theme) + legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) legacyController.statusBar.statusBarStyle = .Hide let baseController = TGViewController(context: legacyController.context)! legacyController.bind(controller: baseController) diff --git a/TelegramUI/OngoingCallContext.swift b/TelegramUI/OngoingCallContext.swift index de75633c0b..ff8dee5952 100644 --- a/TelegramUI/OngoingCallContext.swift +++ b/TelegramUI/OngoingCallContext.swift @@ -44,6 +44,26 @@ private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCal } } +private func ongoingNetworkTypeForType(_ type: NetworkType) -> OngoingCallNetworkType { + switch type { + case .none: + return .wifi + case .wifi: + return .wifi + case let .cellular(cellular): + switch cellular { + case .edge: + return .cellularEdge + case .gprs: + return .cellularGprs + case .thirdG, .unknown: + return .cellular3g + case .lte: + return .cellularLte + } + } +} + final class OngoingCallContext { let internalId: CallSessionInternalId @@ -54,7 +74,8 @@ final class OngoingCallContext { private let contextState = Promise(nil) var state: Signal { - return self.contextState.get() |> map { + return self.contextState.get() + |> map { $0.flatMap { switch $0 { case .initializing: @@ -69,8 +90,9 @@ final class OngoingCallContext { } private let audioSessionDisposable = MetaDisposable() + private var networkTypeDisposable: Disposable? - init(callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?) { + init(callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal) { let _ = setupLogs self.internalId = internalId @@ -87,12 +109,19 @@ final class OngoingCallContext { break } } - let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer) + let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForType(initialNetworkType)) self.contextRef = Unmanaged.passRetained(context) context.stateChanged = { [weak self] state in self?.contextState.set(.single(state)) } } + + self.networkTypeDisposable = (updatedNetworkType + |> deliverOn(self.queue)).start(next: { [weak self] networkType in + self?.withContext { context in + context.setNetworkType(ongoingNetworkTypeForType(networkType)) + } + }) } deinit { @@ -102,6 +131,7 @@ final class OngoingCallContext { } self.audioSessionDisposable.dispose() + self.networkTypeDisposable?.dispose() } private func withContext(_ f: @escaping (OngoingCallThreadLocalContext) -> Void) { @@ -114,7 +144,9 @@ final class OngoingCallContext { } func start(key: Data, isOutgoing: Bool, connections: CallSessionConnectionSet, maxLayer: Int32, audioSessionActive: Signal) { - self.audioSessionDisposable.set((audioSessionActive |> filter { $0 } |> take(1)).start(next: { [weak self] _ in + self.audioSessionDisposable.set((audioSessionActive + |> filter { $0 } + |> take(1)).start(next: { [weak self] _ in if let strongSelf = self { strongSelf.withContext { context in context.start(withKey: key, isOutgoing: isOutgoing, primaryConnection: callConnectionDescription(connections.primary), alternativeConnections: connections.alternatives.map(callConnectionDescription), maxLayer: maxLayer) diff --git a/TelegramUI/OngoingCallThreadLocalContext.h b/TelegramUI/OngoingCallThreadLocalContext.h index 1e1c98965b..c88b33e184 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.h +++ b/TelegramUI/OngoingCallThreadLocalContext.h @@ -21,6 +21,14 @@ typedef NS_ENUM(int32_t, OngoingCallState) { OngoingCallStateFailed }; +typedef NS_ENUM(int32_t, OngoingCallNetworkType) { + OngoingCallNetworkTypeWifi, + OngoingCallNetworkTypeCellularGprs, + OngoingCallNetworkTypeCellularEdge, + OngoingCallNetworkTypeCellular3g, + OngoingCallNetworkTypeCellularLte +}; + @protocol OngoingCallThreadLocalContextQueue - (void)dispatch:(void (^ _Nonnull)())f; @@ -45,11 +53,12 @@ typedef NS_ENUM(int32_t, OngoingCallState) { @property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallState); -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy; +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType; - (void)startWithKey:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer; - (void)stop; - (void)setIsMuted:(bool)isMuted; +- (void)setNetworkType:(OngoingCallNetworkType)networkType; @end diff --git a/TelegramUI/OngoingCallThreadLocalContext.mm b/TelegramUI/OngoingCallThreadLocalContext.mm index 06227b5b3f..10e4851baa 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.mm +++ b/TelegramUI/OngoingCallThreadLocalContext.mm @@ -121,6 +121,7 @@ static void withContext(int32_t contextId, void (^f)(OngoingCallThreadLocalConte id _queue; int32_t _contextId; + OngoingCallNetworkType _networkType; NSTimeInterval _callReceiveTimeout; NSTimeInterval _callRingTimeout; NSTimeInterval _callConnectTimeout; @@ -159,13 +160,28 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat @end +static int callControllerNetworkTypeForType(OngoingCallNetworkType type) { + switch (type) { + case OngoingCallNetworkTypeWifi: + return tgvoip::NET_TYPE_WIFI; + case OngoingCallNetworkTypeCellularGprs: + return tgvoip::NET_TYPE_GPRS; + case OngoingCallNetworkTypeCellular3g: + return tgvoip::NET_TYPE_3G; + case OngoingCallNetworkTypeCellularLte: + return tgvoip::NET_TYPE_LTE; + default: + return tgvoip::NET_TYPE_WIFI; + } +} + @implementation OngoingCallThreadLocalContext + (void)setupLoggingFunction:(void (*)(NSString *))loggingFunction { TGVoipLoggingFunction = loggingFunction; } -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy { +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType { self = [super init]; if (self != nil) { _queue = queue; @@ -178,6 +194,7 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat _callPacketTimeout = 10.0; _dataSavingMode = 0; _allowP2P = true; + _networkType = networkType; _controller = new tgvoip::VoIPController(); _controller->implData = (void *)((intptr_t)_contextId); @@ -185,9 +202,7 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat if (proxy != nil) { _controller->SetProxy(tgvoip::PROXY_SOCKS5, proxy.host.UTF8String, (uint16_t)proxy.port, proxy.username.UTF8String ?: "", proxy.password.UTF8String ?: ""); } - - /*releasable*/ - //_controller->SetStateCallback(&controllerStateCallback); + _controller->SetNetworkType(callControllerNetworkTypeForType(networkType)); auto callbacks = tgvoip::VoIPController::Callbacks(); callbacks.connectionStateChanged = &controllerStateCallback; @@ -316,4 +331,11 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat _controller->SetMicMute(isMuted); } +- (void)setNetworkType:(OngoingCallNetworkType)networkType { + if (_networkType != networkType) { + _networkType = networkType; + _controller->SetNetworkType(callControllerNetworkTypeForType(networkType)); + } +} + @end diff --git a/TelegramUI/PasscodeOptionsController.swift b/TelegramUI/PasscodeOptionsController.swift index 59266f46bc..a85eacab13 100644 --- a/TelegramUI/PasscodeOptionsController.swift +++ b/TelegramUI/PasscodeOptionsController.swift @@ -255,7 +255,8 @@ func passcodeOptionsController(account: Account) -> ViewController { }) }).start() - let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in + let _ = (passcodeOptionsDataPromise.get() + |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in passcodeOptionsDataPromise?.set(.single(data.withUpdatedAccessChallenge(challenge).withUpdatedPresentationSettings(data.presentationSettings.withUpdatedAutolockTimeout(1 * 60 * 60)))) }) @@ -437,7 +438,8 @@ func passcodeOptionsController(account: Account) -> ViewController { public func passcodeOptionsAccessController(account: Account, animateIn: Bool = true, completion: @escaping (Bool) -> Void) -> Signal { return account.postbox.transaction { transaction -> PostboxAccessChallengeData in return transaction.getAccessChallengeData() - } |> deliverOnMainQueue + } + |> deliverOnMainQueue |> map { challenge -> ViewController? in if case .none = challenge { completion(true) @@ -507,3 +509,76 @@ public func passcodeOptionsAccessController(account: Account, animateIn: Bool = } } } + +public func passcodeEntryController(account: Account, animateIn: Bool = true, completion: @escaping (Bool) -> Void) -> Signal { + return account.postbox.transaction { transaction -> PostboxAccessChallengeData in + return transaction.getAccessChallengeData() + } + |> deliverOnMainQueue + |> map { challenge -> ViewController? in + if case .none = challenge { + completion(true) + return nil + } else { + var attemptData: TGPasscodeEntryAttemptData? + if let attempts = challenge.attempts { + attemptData = TGPasscodeEntryAttemptData(numberOfInvalidAttempts: Int(attempts.count), dateOfLastInvalidAttempt: Double(attempts.timestamp)) + } + var dismissImpl: (() -> Void)? + + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true), theme: presentationData.theme) + let mode: TGPasscodeEntryControllerMode + switch challenge { + case .none, .numericalPassword: + mode = TGPasscodeEntryControllerModeVerifySimple + case .plaintextPassword: + mode = TGPasscodeEntryControllerModeVerifyComplex + } + let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: mode, cancelEnabled: true, allowTouchId: false, attemptData: attemptData, completion: { value in + completion(value != nil) + dismissImpl?() + })! + controller.checkCurrentPasscode = { value in + if let value = value { + switch challenge { + case .none: + return true + case let .numericalPassword(code, _, _): + return value == code + case let .plaintextPassword(code, _, _): + return value == code + } + } else { + return false + } + } + controller.updateAttemptData = { attemptData in + let _ = account.postbox.transaction({ transaction -> Void in + var attempts: AccessChallengeAttempts? + if let attemptData = attemptData { + attempts = AccessChallengeAttempts(count: Int32(attemptData.numberOfInvalidAttempts), timestamp: Int32(attemptData.dateOfLastInvalidAttempt)) + } + var data = transaction.getAccessChallengeData() + switch data { + case .none: + break + case let .numericalPassword(value, timeout, _): + data = .numericalPassword(value: value, timeout: timeout, attempts: attempts) + case let .plaintextPassword(value, timeout, _): + data = .plaintextPassword(value: value, timeout: timeout, attempts: attempts) + } + transaction.setAccessChallengeData(data) + }).start() + } + legacyController.bind(controller: controller) + legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) + legacyController.statusBar.statusBarStyle = .White + dismissImpl = { [weak legacyController] in + legacyController?.dismiss() + } + return legacyController + } + } +} diff --git a/TelegramUI/PresentationCall.swift b/TelegramUI/PresentationCall.swift index eb404471fd..bc23e45dfc 100644 --- a/TelegramUI/PresentationCall.swift +++ b/TelegramUI/PresentationCall.swift @@ -231,7 +231,7 @@ 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?, proxyServer: ProxyServerSettings?) { + init(audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, proxyServer: ProxyServerSettings?, currentNetworkType: NetworkType, updatedNetworkType: Signal) { self.audioSession = audioSession self.callSessionManager = callSessionManager self.callKitIntegration = callKitIntegration @@ -241,7 +241,7 @@ public final class PresentationCall { self.isOutgoing = isOutgoing self.peer = peer - self.ongoingGontext = OngoingCallContext(callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer) + self.ongoingGontext = OngoingCallContext(callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: currentNetworkType, updatedNetworkType: updatedNetworkType) var didReceiveAudioOutputs = false self.sessionStateDisposable = (callSessionManager.callState(internalId: internalId) diff --git a/TelegramUI/PresentationCallManager.swift b/TelegramUI/PresentationCallManager.swift index 8f3ba59a64..b32ded80ca 100644 --- a/TelegramUI/PresentationCallManager.swift +++ b/TelegramUI/PresentationCallManager.swift @@ -27,6 +27,7 @@ public enum RequestCallResult { public final class PresentationCallManager { private let postbox: Postbox + private let networkType: Signal private let audioSession: ManagedAudioSession private let callSessionManager: CallSessionManager private let callKitIntegration: CallKitIntegration? @@ -51,8 +52,9 @@ public final class PresentationCallManager { private var proxyServer: ProxyServerSettings? private var proxyServerDisposable: Disposable? - public init(postbox: Postbox, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager) { + public init(postbox: Postbox, networkType: Signal, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager) { self.postbox = postbox + self.networkType = networkType self.audioSession = audioSession self.callSessionManager = callSessionManager @@ -79,24 +81,32 @@ public final class PresentationCallManager { audioSessionActivationChangedImpl?(value) }) - self.ringingStatesDisposable = (callSessionManager.ringingStates() |> mapToSignal { ringingStates -> Signal<[(Peer, CallSessionRingingState)], NoError> in - if ringingStates.isEmpty { - return .single([]) - } else { - return postbox.transaction { transaction -> [(Peer, CallSessionRingingState)] in - var result: [(Peer, CallSessionRingingState)] = [] - for state in ringingStates { - if let peer = transaction.getPeer(state.peerId) { - result.append((peer, state)) - } + self.ringingStatesDisposable = (callSessionManager.ringingStates() + |> mapToSignal { ringingStates -> Signal<[(Peer, CallSessionRingingState)], NoError> in + if ringingStates.isEmpty { + return .single([]) + } else { + return postbox.transaction { transaction -> [(Peer, CallSessionRingingState)] in + var result: [(Peer, CallSessionRingingState)] = [] + for state in ringingStates { + if let peer = transaction.getPeer(state.peerId) { + result.append((peer, state)) } - return result } + return result } } - |> deliverOnMainQueue).start(next: { [weak self] ringingStates in - self?.ringingStatesUpdated(ringingStates) - }) + } + |> mapToSignal { states -> Signal<([(Peer, CallSessionRingingState)], NetworkType), NoError> in + return networkType + |> take(1) + |> map { currentNetworkType -> ([(Peer, CallSessionRingingState)], NetworkType) in + return (states, currentNetworkType) + } + } + |> deliverOnMainQueue).start(next: { [weak self] ringingStates, currentNetworkType in + self?.ringingStatesUpdated(ringingStates, currentNetworkType: currentNetworkType) + }) startCallImpl = { [weak self] uuid, handle in if let strongSelf = self, let userId = Int32(handle) { @@ -151,23 +161,23 @@ public final class PresentationCallManager { self.proxyServerDisposable?.dispose() } - private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState)]) { + private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState)], currentNetworkType: NetworkType) { if let firstState = ringingStates.first { if self.currentCall == nil { - let call = PresentationCall(audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: self.callKitIntegration, internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0, proxyServer: self.proxyServer) + let call = PresentationCall(audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: self.callKitIntegration, internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0, proxyServer: self.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: self.networkType) self.currentCall = call self.currentCallPromise.set(.single(call)) self.hasActiveCallsPromise.set(true) self.removeCurrentCallDisposable.set((call.canBeRemoved - |> deliverOnMainQueue).start(next: { [weak self, 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) - } + |> deliverOnMainQueue).start(next: { [weak self, 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) } - })) + } + })) } } } @@ -178,12 +188,12 @@ public final class PresentationCallManager { } 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) - } - })) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self { + strongSelf.callKitIntegration?.startCall(peerId: peerId, displayTitle: peer.displayTitle) + } + })) } else { let _ = self.startCall(peerId: peerId).start() } @@ -191,27 +201,31 @@ public final class PresentationCallManager { } private func startCall(peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal { - return (self.callSessionManager.request(peerId: peerId, internalId: internalId) |> deliverOnMainQueue |> beforeNext { [weak self] internalId in + return (combineLatest(self.callSessionManager.request(peerId: peerId, internalId: internalId), self.networkType |> take(1)) + |> deliverOnMainQueue + |> beforeNext { [weak self] internalId, currentNetworkType in if let strongSelf = self { if let currentCall = strongSelf.currentCall { currentCall.rejectBusy() } - let call = PresentationCall(audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: strongSelf.callKitIntegration, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, proxyServer: strongSelf.proxyServer) + let call = PresentationCall(audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: strongSelf.callKitIntegration, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, 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) - } + |> 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 { _ -> Signal in return .single(true) } + }) + |> mapToSignal { _ -> Signal in + return .single(true) + } } } diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index 28b0023823..b21a988e25 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -15,6 +15,7 @@ enum PresentationResourceKey: Int32 { case navigationComposeIcon case navigationCallIcon + case navigationInfoIcon case navigationShareIcon case navigationSearchIcon case navigationCompactSearchIcon diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index 7439eeb032..6a5e4dae4e 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -87,6 +87,12 @@ struct PresentationResourcesRootController { }) } + static func navigationInfoIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationInfoIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/InfoIcon"), color: theme.rootController.navigationBar.accentTextColor) + }) + } + static func navigationSearchIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationSearchIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat List/SearchIcon"), color: theme.rootController.navigationBar.accentTextColor) diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index b6f5c53ce1..71b9bcd046 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -767,7 +767,7 @@ public enum PresentationThemeName: Equatable { } } -public final class PresentationTheme { +public final class PresentationTheme: Equatable { public let name: PresentationThemeName public let overallDarkAppearance: Bool public let allowsCustomWallpapers: Bool @@ -799,4 +799,8 @@ public final class PresentationTheme { public func object(_ key: Int32, _ generate: (PresentationTheme) -> AnyObject?) -> AnyObject? { return self.resourceCache.object(key, self, generate) } + + public static func ==(lhs: PresentationTheme, rhs: PresentationTheme) -> Bool { + return lhs === rhs + } } diff --git a/TelegramUI/SecureIdAuthController.swift b/TelegramUI/SecureIdAuthController.swift index 93649e17c7..0821c1e645 100644 --- a/TelegramUI/SecureIdAuthController.swift +++ b/TelegramUI/SecureIdAuthController.swift @@ -9,15 +9,17 @@ final class SecureIdAuthControllerInteraction { let updateState: ((SecureIdAuthControllerState) -> SecureIdAuthControllerState) -> Void let present: (ViewController, Any?) -> Void let checkPassword: (String) -> Void + let setupPassword: () -> Void let grant: () -> Void let openUrl: (String) -> Void let openMention: (TelegramPeerMention) -> Void let deleteAll: () -> Void - fileprivate init(updateState: @escaping ((SecureIdAuthControllerState) -> SecureIdAuthControllerState) -> Void, present: @escaping (ViewController, Any?) -> Void, checkPassword: @escaping (String) -> Void, grant: @escaping () -> Void, openUrl: @escaping (String) -> Void, openMention: @escaping (TelegramPeerMention) -> Void, deleteAll: @escaping () -> Void) { + fileprivate init(updateState: @escaping ((SecureIdAuthControllerState) -> SecureIdAuthControllerState) -> Void, present: @escaping (ViewController, Any?) -> Void, checkPassword: @escaping (String) -> Void, setupPassword: @escaping () -> Void, grant: @escaping () -> Void, openUrl: @escaping (String) -> Void, openMention: @escaping (TelegramPeerMention) -> Void, deleteAll: @escaping () -> Void) { self.updateState = updateState self.present = present self.checkPassword = checkPassword + self.setupPassword = setupPassword self.grant = grant self.openUrl = openUrl self.openMention = openMention @@ -52,6 +54,7 @@ final class SecureIdAuthController: ViewController { init(account: Account, mode: SecureIdAuthControllerMode) { self.account = account self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.mode = mode switch mode { @@ -67,6 +70,7 @@ final class SecureIdAuthController: ViewController { self.title = self.presentationData.strings.Passport_Title self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationInfoIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.infoPressed)) self.challengeDisposable.set((twoStepAuthData(account.network) |> deliverOnMainQueue).start(next: { [weak self] data in @@ -123,7 +127,7 @@ final class SecureIdAuthController: ViewController { |> deliverOnMainQueue).start(next: { [weak self] values, configuration in if let strongSelf = self { strongSelf.updateState { state in - var state = state + let state = state let primaryLanguageByCountry = configuration.nativeLanguageByCountry switch state { @@ -173,67 +177,9 @@ final class SecureIdAuthController: ViewController { }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, checkPassword: { [weak self] password in - if let strongSelf = self { - if let verificationState = strongSelf.state.verificationState, case let .passwordChallenge(hint, challengeState) = verificationState { - switch challengeState { - case .none, .invalid: - break - case .checking: - return - } - strongSelf.updateState { state in - var state = state - state.verificationState = .passwordChallenge(hint, .checking) - return state - } - strongSelf.challengeDisposable.set((accessSecureId(network: strongSelf.account.network, password: password) - |> deliverOnMainQueue).start(next: { context in - if let strongSelf = self, let verificationState = strongSelf.state.verificationState, case .passwordChallenge(_, .checking) = verificationState { - strongSelf.updateState { 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: { error in - if let strongSelf = self { - let errorText: String - switch error { - case let .passwordError(passwordError): - switch passwordError { - case .invalidPassword: - errorText = strongSelf.presentationData.strings.LoginPassword_InvalidPasswordError - case .limitExceeded: - errorText = strongSelf.presentationData.strings.LoginPassword_FloodError - case .generic: - errorText = strongSelf.presentationData.strings.Login_UnknownError - } - case .generic: - errorText = strongSelf.presentationData.strings.Login_UnknownError - case .secretPasswordMismatch: - 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)) - - if let verificationState = strongSelf.state.verificationState, case let .passwordChallenge(hint, .checking) = verificationState { - strongSelf.updateState { state in - var state = state - state.verificationState = .passwordChallenge(hint, .invalid) - return state - } - } - } - })) - } - } + self?.checkPassword(password: password, inBackground: false, completion: {}) + }, setupPassword: { [weak self] in + self?.setupPassword() }, grant: { [weak self] in self?.grantAccess() }, openUrl: { [weak self] url in @@ -267,7 +213,7 @@ final class SecureIdAuthController: ViewController { guard let strongSelf = self else { return } - strongSelf.navigationItem.rightBarButtonItem = nil + strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationInfoIcon(strongSelf.presentationData.theme), style: .plain, target: self, action: #selector(strongSelf.infoPressed)) strongSelf.updateState { state in if case var .list(list) = state { list.values = [] @@ -290,7 +236,7 @@ final class SecureIdAuthController: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } - private func updateState(_ f: (SecureIdAuthControllerState) -> SecureIdAuthControllerState) { + private func updateState(animated: Bool = true, _ f: (SecureIdAuthControllerState) -> SecureIdAuthControllerState) { let state = f(self.state) if state != self.state { var previousHadProgress = false @@ -312,7 +258,7 @@ final class SecureIdAuthController: ViewController { let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: self.presentationData.theme)) self.navigationItem.rightBarButtonItem = item } else { - self.navigationItem.rightBarButtonItem = nil + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationInfoIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.infoPressed)) } } } @@ -322,6 +268,94 @@ final class SecureIdAuthController: ViewController { self.dismiss() } + @objc private func checkPassword(password: String, inBackground: Bool, completion: @escaping () -> Void) { + if let verificationState = self.state.verificationState, case let .passwordChallenge(hint, challengeState) = verificationState { + switch challengeState { + case .none, .invalid: + break + case .checking: + return + } + self.updateState(animated: !inBackground, { state in + var state = state + state.verificationState = .passwordChallenge(hint, .checking) + return state + }) + self.challengeDisposable.set((accessSecureId(network: self.account.network, password: password) + |> deliverOnMainQueue).start(next: { [weak self] context in + guard let strongSelf = self, let verificationState = strongSelf.state.verificationState, case .passwordChallenge(_, .checking) = verificationState else { + return + } + strongSelf.updateState(animated: !inBackground, { 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 + }) + completion() + }, error: { [weak self] error in + guard let strongSelf = self else { + return + } + let errorText: String + switch error { + case let .passwordError(passwordError): + switch passwordError { + case .invalidPassword: + errorText = strongSelf.presentationData.strings.LoginPassword_InvalidPasswordError + case .limitExceeded: + errorText = strongSelf.presentationData.strings.LoginPassword_FloodError + case .generic: + errorText = strongSelf.presentationData.strings.Login_UnknownError + } + case .generic: + errorText = strongSelf.presentationData.strings.Login_UnknownError + case .secretPasswordMismatch: + 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)) + + if let verificationState = strongSelf.state.verificationState, case let .passwordChallenge(hint, .checking) = verificationState { + strongSelf.updateState(animated: !inBackground, { state in + var state = state + state.verificationState = .passwordChallenge(hint, .invalid) + return state + }) + } + completion() + })) + } + } + + @objc private func setupPassword() { + var completionImpl: ((String, String) -> Void)? + let controller = createPasswordController(account: self.account, completion: { password, hint in + completionImpl?(password, hint) + }) + completionImpl = { [weak self, weak controller] password, hint in + guard let strongSelf = self else { + controller?.dismiss() + return + } + strongSelf.updateState(animated: false, { state in + var state = state + state.verificationState = .passwordChallenge(hint, .none) + return state + }) + strongSelf.checkPassword(password: password, inBackground: true, completion: { + controller?.dismiss() + }) + } + self.present(controller, in: .window(.root)) + } + @objc private func grantAccess() { switch self.state { case let .form(form): @@ -335,4 +369,15 @@ final class SecureIdAuthController: ViewController { break } } + + @objc private func infoPressed() { + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: self.presentationData.theme), title: self.presentationData.strings.Passport_InfoTitle, text: self.presentationData.strings.Passport_InfoText.replacingOccurrences(of: "**", with: ""), actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Passport_InfoLearnMore, action: { [weak self] in + guard let strongSelf = self else { + return + } + openExternalUrl(account: strongSelf.account, url: strongSelf.presentationData.strings.Passport_InfoFAQ_URL, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { + self?.view.endEditing(true) + }) + })]), in: .window(.root)) + } } diff --git a/TelegramUI/SecureIdAuthControllerNode.swift b/TelegramUI/SecureIdAuthControllerNode.swift index 05be5d56fe..acb57b3d97 100644 --- a/TelegramUI/SecureIdAuthControllerNode.swift +++ b/TelegramUI/SecureIdAuthControllerNode.swift @@ -80,6 +80,8 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { } else { if self.contentNode is SecureIdAuthListContentNode { contentSpacing = 16.0 + } else if self.contentNode is SecureIdAuthPasswordSetupContentNode { + contentSpacing = 24.0 } else { contentSpacing = 56.0 } @@ -119,7 +121,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { contentNode.didAppear() if transition.isAnimated { contentNode.animateIn() - if !(contentNode is SecureIdAuthPasswordOptionContentNode) { + if !(contentNode is SecureIdAuthPasswordOptionContentNode || contentNode is SecureIdAuthPasswordSetupContentNode) { transition.animatePositionAdditive(node: contentNode, offset: CGPoint(x: layout.size.width, y: 0.0)) } } @@ -170,6 +172,14 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { var contentNode: (ASDisplayNode & SecureIdAuthContentNode)? switch verificationState { + case .noChallenge: + if let _ = self.contentNode as? SecureIdAuthPasswordSetupContentNode { + } else { + let current = SecureIdAuthPasswordSetupContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, setupPassword: { [weak self] in + self?.interaction.setupPassword() + }) + contentNode = current + } case let .passwordChallenge(hint, challengeState): if let current = self.contentNode as? SecureIdAuthPasswordOptionContentNode { current.updateIsChecking(challengeState == .checking) @@ -187,8 +197,6 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { current.updateIsChecking(challengeState == .checking) contentNode = current } - case .noChallenge: - contentNode = nil case .verified: if let encryptedFormData = form.encryptedFormData, let formData = form.formData { if let current = self.contentNode as? SecureIdAuthFormContentNode { diff --git a/TelegramUI/SecureIdAuthHeaderNode.swift b/TelegramUI/SecureIdAuthHeaderNode.swift index bce971b4a7..5b4ec17260 100644 --- a/TelegramUI/SecureIdAuthHeaderNode.swift +++ b/TelegramUI/SecureIdAuthHeaderNode.swift @@ -80,14 +80,28 @@ final class SecureIdAuthHeaderNode: ASDisplayNode { 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)) - let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: avatarSize.height + avatarTitleSpacing), size: titleSize) + var titleOffset: CGFloat = 0.0 + if !self.serviceAvatarNode.alpha.isZero { + titleOffset = avatarSize.height + avatarTitleSpacing + } + + let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: titleOffset), size: titleSize) ContainedViewLayoutTransition.immediate.updateFrame(node: self.titleNode, frame: titleFrame) - let resultHeight: CGFloat = avatarSize.height + avatarTitleSpacing + titleSize.height + var resultHeight: CGFloat = titleSize.height + if !self.serviceAvatarNode.alpha.isZero { + resultHeight += avatarSize.height + avatarTitleSpacing + } return resultHeight } } diff --git a/TelegramUI/SecureIdAuthPasswordSetupContentNode.swift b/TelegramUI/SecureIdAuthPasswordSetupContentNode.swift new file mode 100644 index 0000000000..3772f51c89 --- /dev/null +++ b/TelegramUI/SecureIdAuthPasswordSetupContentNode.swift @@ -0,0 +1,134 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let titleFont = Font.regular(14.0) +private let buttonFont = Font.regular(17.0) + +final class SecureIdAuthPasswordSetupContentNode: ASDisplayNode, SecureIdAuthContentNode, UITextFieldDelegate { + private let setupPassword: () -> Void + + private let iconNode: ASImageNode + private let titleNode: ImmediateTextNode + private let buttonTopSeparatorNode: ASDisplayNode + private let buttonBackgroundNode: ASDisplayNode + private let buttonBottomSeparatorNode: ASDisplayNode + private let buttonHighlightedBackgroundNode: ASDisplayNode + private let buttonTextNode: ImmediateTextNode + private let buttonNode: HighlightableButtonNode + + private var validLayout: CGFloat? + + init(theme: PresentationTheme, strings: PresentationStrings, setupPassword: @escaping () -> Void) { + self.setupPassword = setupPassword + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displayWithoutProcessing = true + self.iconNode.displaysAsynchronously = false + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Secure ID/EmptyPasswordIcon"), color: theme.list.freeMonoIcon) + + self.titleNode = ImmediateTextNode() + self.titleNode.attributedText = NSAttributedString(string: strings.Passport_PasswordDescription, font: Font.regular(14.0), textColor: theme.list.freeTextColor) + self.titleNode.maximumNumberOfLines = 0 + self.titleNode.textAlignment = .center + + self.buttonTopSeparatorNode = ASDisplayNode() + self.buttonTopSeparatorNode.isLayerBacked = true + self.buttonTopSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor + self.buttonBottomSeparatorNode = ASDisplayNode() + self.buttonBottomSeparatorNode.isLayerBacked = true + self.buttonBottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor + self.buttonBackgroundNode = ASDisplayNode() + self.buttonBackgroundNode.isLayerBacked = true + self.buttonBackgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor + self.buttonHighlightedBackgroundNode = ASDisplayNode() + self.buttonHighlightedBackgroundNode.isLayerBacked = true + self.buttonHighlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor + self.buttonHighlightedBackgroundNode.alpha = 0.0 + self.buttonTextNode = ImmediateTextNode() + self.buttonTextNode.attributedText = NSAttributedString(string: strings.Passport_PasswordCreate, font: buttonFont, textColor: theme.list.itemAccentColor) + self.buttonTextNode.maximumNumberOfLines = 1 + self.buttonTextNode.textAlignment = .center + self.buttonNode = HighlightableButtonNode() + + super.init() + + self.addSubnode(self.iconNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.buttonBackgroundNode) + self.addSubnode(self.buttonTopSeparatorNode) + self.addSubnode(self.buttonBottomSeparatorNode) + self.addSubnode(self.buttonHighlightedBackgroundNode) + self.addSubnode(self.buttonTextNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.buttonHighlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.buttonHighlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.buttonHighlightedBackgroundNode.alpha = 0.0 + strongSelf.buttonHighlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> SecureIdAuthContentLayout { + let transition = self.validLayout == nil ? .immediate : transition + self.validLayout = width + + var iconSize = self.iconNode.image?.size ?? CGSize() + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((width - iconSize.width) / 2.0), y: -50.0), size: iconSize)) + iconSize.height = max(0.0, iconSize.height - 100.0) + + let titleSize = self.titleNode.updateLayout(CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude)) + + let buttonTitleSize = self.buttonTextNode.updateLayout(CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude)) + + let buttonSize = CGSize(width: width, height: 44.0) + + let iconSpacing: CGFloat = 30.0 + let titleSpacing: CGFloat = 24.0 + + let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: iconSize.height + iconSpacing), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + let buttonFrame = CGRect(origin: CGPoint(x: 0.0, y: titleFrame.maxY + titleSpacing), size: buttonSize) + + transition.updateFrame(node: self.buttonTopSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonFrame.minY), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.buttonBottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonFrame.maxY - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.buttonBackgroundNode, frame: buttonFrame) + transition.updateFrame(node: self.buttonHighlightedBackgroundNode, frame: buttonFrame) + transition.updateFrame(node: self.buttonTextNode, frame: CGRect(origin: CGPoint(x: 16.0, y: buttonFrame.minY + floor((buttonSize.height - buttonTitleSize.height) / 2.0)), size: buttonTitleSize)) + transition.updateFrame(node: self.buttonNode, frame: buttonFrame) + + let contentHeight = buttonFrame.maxY + + return SecureIdAuthContentLayout(height: contentHeight, centerOffset: floor(contentHeight / 2.0)) + } + + func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func didAppear() { + } + + func willDisappear() { + } + + @objc private func buttonPressed() { + self.setupPassword() + } +} diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index 5261a93692..e776147cae 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -130,11 +130,11 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone } public func applicationStatusBarOrientation() -> UIInterfaceOrientation { - return legacyComponentsApplication.statusBarOrientation + return legacyComponentsApplication?.statusBarOrientation ?? UIInterfaceOrientation.portrait } public func statusBarFrame() -> CGRect { - return legacyComponentsApplication.statusBarFrame + return legacyComponentsApplication?.statusBarFrame ?? CGRect(origin: CGPoint(), size: CGSize(width: 320.0, height: 20.0)) } public func isStatusBarHidden() -> Bool { @@ -395,7 +395,7 @@ public func setupLegacyComponents(account: Account) { legacyAccount = account } -public func initializeLegacyComponents(application: UIApplication, currentSizeClassGetter: @escaping () -> UIUserInterfaceSizeClass, currentHorizontalClassGetter: @escaping () -> UIUserInterfaceSizeClass, documentsPath: String, currentApplicationBounds: @escaping () -> CGRect, canOpenUrl: @escaping (URL) -> Bool, openUrl: @escaping (URL) -> Void) { +public func initializeLegacyComponents(application: UIApplication?, currentSizeClassGetter: @escaping () -> UIUserInterfaceSizeClass, currentHorizontalClassGetter: @escaping () -> UIUserInterfaceSizeClass, documentsPath: String, currentApplicationBounds: @escaping () -> CGRect, canOpenUrl: @escaping (URL) -> Bool, openUrl: @escaping (URL) -> Void) { legacyComponentsApplication = application legacyCanOpenUrl = canOpenUrl legacyOpenUrl = openUrl @@ -415,6 +415,5 @@ public func initializeLegacyComponents(application: UIApplication, currentSizeCl return legacyAccount })) ASActor.registerClass(LegacyImageDownloadActor.self) - LegacyComponentsGlobals.setProvider(LegacyComponentsGlobalsProviderImpl()) }