diff --git a/Telegram/Telegram-iOS/Resources/Delete1.tgs b/Telegram/Telegram-iOS/Resources/Delete1.tgs new file mode 100644 index 0000000000..5a62b5603f Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/Delete1.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/Delete2.tgs b/Telegram/Telegram-iOS/Resources/Delete2.tgs new file mode 100644 index 0000000000..d17ced8b07 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/Delete2.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/Delete3.tgs b/Telegram/Telegram-iOS/Resources/Delete3.tgs new file mode 100644 index 0000000000..c65577a62a Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/Delete3.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/Delete4.tgs b/Telegram/Telegram-iOS/Resources/Delete4.tgs new file mode 100644 index 0000000000..1887ea4648 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/Delete4.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/Delete5.tgs b/Telegram/Telegram-iOS/Resources/Delete5.tgs new file mode 100644 index 0000000000..b4d0d40956 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/Delete5.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/chunk2.ch2.q1.mp4 b/Telegram/Telegram-iOS/Resources/chunk2.ch2.q1.mp4 deleted file mode 100644 index b513f5f83d..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/chunk2.ch2.q1.mp4 and /dev/null differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 53975e99e0..6a132802cb 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7770,6 +7770,8 @@ Sorry for the inconvenience."; "DeleteAccount.DeleteMyAccountTitle" = "Delete My Account"; "DeleteAccount.DeleteMyAccount" = "Delete My Account"; +"DeleteAccount.SavedMessages" = "Saved"; + "DeleteAccount.ComeBackLater" = "Come Back Later"; "DeleteAccount.Continue" = "Continue"; @@ -7778,10 +7780,18 @@ Sorry for the inconvenience."; "DeleteAccount.GroupsAndChannelsTitle" = "Your Groups and Channels"; "DeleteAccount.GroupsAndChannelsText" = "The groups and channels you created will either get new admins or become orphaned."; -"DeleteAccount.GroupsAndChannelsInfo" = "You can transfer group and channel ownership to other users via Chat Info > Edit > Admins. [More info]()"; +"DeleteAccount.GroupsAndChannelsInfo" = "You can transfer group and channel ownership to other users via Chat Info > Edit > Admins."; "DeleteAccount.MessageHistoryTitle" = "Your Message History"; -"DeleteAccount.MessageHistoryText" = "Your chat partners will keep their message history with you, including the messages you shared in secret chats.\n\nYou can remove any messages for both sides at any time, but this will not be possible if you delete your account. [More info]()"; +"DeleteAccount.MessageHistoryText" = "Your chat partners will keep their message history with you, including the messages you shared in secret chats.\n\nYou can remove any messages for both sides at any time, but this will not be possible if you delete your account."; + +"DeleteAccount.DeleteMessagesURL" = "https://telegram.org/faq#q-can-i-delete-my-messages"; + +"DeleteAccount.EnterPhoneNumber" = "Enter Your Phone Number"; +"DeleteAccount.InvalidPhoneNumberError" = "Invalid phone number. Please try again."; + +"DeleteAccount.EnterPassword" = "Enter Your Password"; +"DeleteAccount.InvalidPasswordError" = "Invalid password. Please try again."; "DeleteAccount.ConfirmationAlertTitle" = "Proceed to Delete Your Account?"; "DeleteAccount.ConfirmationAlertText" = "Deleting your account will permanently delete your data!\n\nIt is imposible to reverse this action!"; @@ -7801,3 +7811,8 @@ Sorry for the inconvenience."; "Premium.Gift.Years_1" = "%@ Year"; "Premium.Gift.Years_any" = "%@ Years"; + +"Premium.GiftedTitle.3Month" = "[%@]() has gifted you a 3-month subscription for Telegram Premium"; +"Premium.GiftedTitle.6Month" = "[%@]() has gifted you a 6-month subscription for Telegram Premium"; +"Premium.GiftedTitle.12Month" = "[%@]() has gifted you a 12-month subscription for Telegram Premium"; +"Premium.GiftedDescription" = "You now have access to additional features."; diff --git a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift index 4bef020a8b..fd1bc9b262 100644 --- a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift @@ -120,7 +120,7 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { peersNode = currentPeersNode peersNode.updateThemeAndStrings(theme: item.theme, strings: item.strings) } else { - peersNode = ChatListSearchRecentPeersNode(context: item.context, theme: item.theme, mode: .list, strings: item.strings, peerSelected: { peer in + peersNode = ChatListSearchRecentPeersNode(context: item.context, theme: item.theme, mode: .list(compact: false), strings: item.strings, peerSelected: { peer in self?.item?.peerSelected(peer) }, peerContextAction: { peer, node, gesture in self?.item?.peerContextAction(peer, node, gesture) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index eaacde87bd..43e43813bc 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -1271,7 +1271,7 @@ open class NavigationController: UINavigationController, ContainableController, let badgeNode = ASImageNode() badgeNode.displaysAsynchronously = false - badgeNode.image = UIImage(bundleImageName: "Components/BadgeTest") + badgeNode.image = UIImage(bundleImageName: "Components/AppBadge") self.badgeNode = badgeNode self.displayNode.addSubnode(badgeNode) } diff --git a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift index 8f5d5d0928..5051bbe78f 100644 --- a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift +++ b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift @@ -12,7 +12,7 @@ import ContextUI import AccountContext public enum HorizontalPeerItemMode { - case list + case list(compact: Bool) case actionSheet } @@ -25,13 +25,13 @@ public final class HorizontalPeerItem: ListViewItem { let context: AccountContext public let peer: EnginePeer let action: (EnginePeer) -> Void - let contextAction: (EnginePeer, ASDisplayNode, ContextGesture?) -> Void + let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?) -> Void)? let isPeerSelected: (EnginePeer.Id) -> Bool let customWidth: CGFloat? let presence: EnginePeer.Presence? let unreadBadge: (Int32, Bool)? - public init(theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, context: AccountContext, peer: EnginePeer, presence: EnginePeer.Presence?, unreadBadge: (Int32, Bool)?, action: @escaping (EnginePeer) -> Void, contextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (EnginePeer.Id) -> Bool, customWidth: CGFloat?) { + public init(theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, context: AccountContext, peer: EnginePeer, presence: EnginePeer.Presence?, unreadBadge: (Int32, Bool)?, action: @escaping (EnginePeer) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?) -> Void)?, isPeerSelected: @escaping (EnginePeer.Id) -> Bool, customWidth: CGFloat?) { self.theme = theme self.strings = strings self.mode = mode @@ -111,11 +111,6 @@ public final class HorizontalPeerItemNode: ListViewItemNode { item.action(item.peer) } } - self.peerNode.contextAction = { [weak self] node, gesture in - if let item = self?.item { - item.contextAction(item.peer, node, gesture) - } - } } override public func didLoad() { @@ -186,10 +181,25 @@ public final class HorizontalPeerItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.peerNode.theme = itemTheme + if case let .list(compact) = item.mode { + strongSelf.peerNode.compact = compact + } else { + strongSelf.peerNode.compact = false + } strongSelf.peerNode.setup(context: item.context, theme: item.theme, strings: item.strings, peer: EngineRenderedPeer(peer: item.peer), numberOfLines: 1, synchronousLoad: synchronousLoads) strongSelf.peerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.size) strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false) + if let contextAction = item.contextAction { + strongSelf.peerNode.contextAction = { [weak item] node, gesture in + if let item = item { + contextAction(item.peer, node, gesture) + } + } + } else { + strongSelf.peerNode.contextAction = nil + } + let badgeBackgroundWidth: CGFloat if let currentBadgeBackgroundImage = currentBadgeBackgroundImage { strongSelf.badgeBackgroundNode.image = currentBadgeBackgroundImage diff --git a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift index 6c7a0d04db..e5bdbe1cde 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift @@ -113,8 +113,9 @@ class InviteLinkHeaderItemNode: ListViewItemNode { let makeTextLayout = TextNode.asyncLayout(self.textNode) return { item, params, neighbors in - let leftInset: CGFloat = 28.0 + params.leftInset - let topInset: CGFloat = 124.0 + let leftInset: CGFloat = 24.0 + params.leftInset + let iconSize = CGSize(width: 140.0, height: 140.0) + let topInset: CGFloat = iconSize.height - 4.0 let spacing: CGFloat = 5.0 let attributedTitle = NSAttributedString(string: item.title ?? "", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) @@ -123,9 +124,9 @@ class InviteLinkHeaderItemNode: ListViewItemNode { return (TelegramTextAttributes.URL, contents) })) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) var contentSize = CGSize(width: params.width, height: topInset + textLayout.size.height) if let _ = item.title { @@ -138,13 +139,12 @@ class InviteLinkHeaderItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { if strongSelf.item == nil { - strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.animationName), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.animationName), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) strongSelf.animationNode.visibility = true } strongSelf.item = item strongSelf.accessibilityLabel = attributedText.string - let iconSize = CGSize(width: 128.0, height: 128.0) strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize) strongSelf.animationNode.updateLayout(size: iconSize) diff --git a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift index 9069fab68b..f5fe398dd5 100644 --- a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift +++ b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift @@ -275,9 +275,8 @@ class GiftAvatarComponent: Component { } self.hasIdleAnimations = component.hasIdleAnimations - let avatarSize = CGSize(width: 100.0, height: 100.0) - self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: component.peer, size: avatarSize, font: avatarPlaceholderFont(size: 78.0), fullSize: true)) + self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: component.peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 63.0), size: avatarSize) return availableSize diff --git a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index 48643b1b3b..c590dfa764 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -84,6 +84,8 @@ public final class SelectablePeerNode: ASDisplayNode { private var peer: EngineRenderedPeer? + public var compact = false + public var theme: SelectablePeerNodeTheme = SelectablePeerNodeTheme(textColor: .black, secretTextColor: .green, selectedTextColor: .blue, checkBackgroundColor: .white, checkFillColor: .blue, checkColor: .white, avatarPlaceholderColor: .white) { didSet { if !self.theme.isEqual(to: oldValue) { @@ -147,7 +149,7 @@ public final class SelectablePeerNode: ASDisplayNode { let text: String var overrideImage: AvatarNodeImageOverride? if peer.peerId == context.account.peerId { - text = strings.DialogList_SavedMessages + text = self.compact ? strings.DeleteAccount_SavedMessages : strings.DialogList_SavedMessages overrideImage = .savedMessagesIcon } else if peer.peerId.isReplies { text = strings.DialogList_Replies diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 3e2c5fb18d..6f77d2566e 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -102,6 +102,7 @@ swift_library( "//submodules/PaymentMethodUI:PaymentMethodUI", "//submodules/PremiumUI:PremiumUI", "//submodules/InviteLinksUI:InviteLinksUI", + "//submodules/HorizontalPeerItem:HorizontalPeerItem", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberControllerNode.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberControllerNode.swift index 9903e8b9ff..41ea069963 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberControllerNode.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberControllerNode.swift @@ -286,5 +286,4 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode { @objc func countryPressed() { self.selectCountryCode?() } - } diff --git a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift index 9c1886c473..8396eb6eb5 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift @@ -13,25 +13,47 @@ import AlertUI import PresentationDataUtils import UrlHandling import InviteLinksUI +import CountrySelectionUI +import PhoneInputNode private struct DeleteAccountDataArguments { let context: AccountContext let openLink: (String) -> Void + let selectCountryCode: () -> Void + let updatePassword: (String) -> Void + let proceed: () -> Void } private enum DeleteAccountDataSection: Int32 { + case header case main } +private enum DeleteAccountEntryTag: Equatable, ItemListItemTag { + case password + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? DeleteAccountEntryTag { + return self == other + } else { + return false + } + } +} + + private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable { case header(PresentationTheme, String, String, String) - - case peers(PresentationTheme, [Peer]) + case peers(PresentationTheme, [EnginePeer]) + case phone(PresentationTheme, PresentationStrings) + case password(PresentationTheme, String) case info(PresentationTheme, String) var section: ItemListSectionId { switch self { - case .header, .peers, .info: + case .header: + return DeleteAccountDataSection.header.rawValue + case .peers, .info, .phone, .password: return DeleteAccountDataSection.main.rawValue } } @@ -43,7 +65,11 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable { case .peers: return 1 case .info: + return 2 + case .phone: return 3 + case .password: + return 4 } } @@ -56,7 +82,7 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable { return false } case let .peers(lhsTheme, lhsPeers): - if case let .peers(rhsTheme, rhsPeers) = rhs, lhsTheme === rhsTheme, arePeerArraysEqual(lhsPeers, rhsPeers) { + if case let .peers(rhsTheme, rhsPeers) = rhs, lhsTheme === rhsTheme, lhsPeers == rhsPeers { return true } else { return false @@ -67,6 +93,19 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable { } else { return false } + case let .phone(lhsTheme, lhsStrings): + if case let .phone(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + case let .password(lhsTheme, lhsPlaceholder): + if case let .password(rhsTheme, rhsPlaceholder) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder { + return true + } else { + return false + } + } } @@ -80,14 +119,26 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable { case let .header(theme, animation, title, text): return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: text, animationName: animation, sectionId: self.section, linkAction: nil) case let .peers(_, peers): - return ItemListTextItem(presentationData: presentationData, text: .plain(peers.first?.debugDisplayTitle ?? ""), sectionId: self.section) + return DeleteAccountPeersItem(context: arguments.context, theme: presentationData.theme, strings: presentationData.strings, peers: peers, sectionId: self.section) case let .info(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) + case .phone: + return DeleteAccountPhoneItem(theme: presentationData.theme, strings: presentationData.strings, value: (nil, nil, ""), sectionId: self.section, selectCountryCode: { + arguments.selectCountryCode() + }, updated: { _ in + + }) + case let .password(_, placeholder): + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: "", placeholder: placeholder, type: .password, returnKeyType: .done, tag: DeleteAccountEntryTag.password, sectionId: self.section, textUpdated: { value in + arguments.updatePassword(value) + }, action: { + arguments.proceed() + }) } } } -private func deleteAccountDataEntries(presentationData: PresentationData, mode: DeleteAccountDataMode, peers: [Peer]) -> [DeleteAccountDataEntry] { +private func deleteAccountDataEntries(presentationData: PresentationData, mode: DeleteAccountDataMode, peers: [EnginePeer]) -> [DeleteAccountDataEntry] { var entries: [DeleteAccountDataEntry] = [] let headerTitle: String @@ -96,24 +147,45 @@ private func deleteAccountDataEntries(presentationData: PresentationData, mode: switch mode { case .peers: - headerAnimation = "" + headerAnimation = "Delete1" headerTitle = presentationData.strings.DeleteAccount_CloudStorageTitle headerText = presentationData.strings.DeleteAccount_CloudStorageText case .groups: - headerAnimation = "" + headerAnimation = "Delete2" headerTitle = presentationData.strings.DeleteAccount_GroupsAndChannelsTitle headerText = presentationData.strings.DeleteAccount_GroupsAndChannelsText case .messages: - headerAnimation = "" + headerAnimation = "Delete3" headerTitle = presentationData.strings.DeleteAccount_MessageHistoryTitle headerText = presentationData.strings.DeleteAccount_MessageHistoryText + case .phone: + headerAnimation = "Delete4" + headerTitle = presentationData.strings.DeleteAccount_EnterPhoneNumber + headerText = "" + case .password: + headerAnimation = "Delete5" + headerTitle = presentationData.strings.DeleteAccount_EnterPassword + headerText = "" } entries.append(.header(presentationData.theme, headerAnimation, headerTitle, headerText)) - entries.append(.peers(presentationData.theme, peers)) - if case .groups = mode { - entries.append(.info(presentationData.theme, presentationData.strings.DeleteAccount_GroupsAndChannelsInfo)) + switch mode { + case .peers: + if !peers.isEmpty { + entries.append(.peers(presentationData.theme, peers)) + } + case .groups: + if !peers.isEmpty { + entries.append(.peers(presentationData.theme, peers)) + entries.append(.info(presentationData.theme, presentationData.strings.DeleteAccount_GroupsAndChannelsInfo)) + } + case .messages: + break + case .phone: + entries.append(.phone(presentationData.theme, presentationData.strings)) + case .password: + entries.append(.password(presentationData.theme, presentationData.strings.LoginPassword_PasswordPlaceholder)) } return entries @@ -121,55 +193,150 @@ private func deleteAccountDataEntries(presentationData: PresentationData, mode: enum DeleteAccountDataMode { case peers - case groups + case groups([EnginePeer]) case messages + case phone + case password } -func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDataMode) -> ViewController { +private struct DeleteAccountDataState: Equatable { + var password: String + var isLoading: Bool + + static func == (lhs: DeleteAccountDataState, rhs: DeleteAccountDataState) -> Bool { + return lhs.password == rhs.password && lhs.isLoading == rhs.isLoading + } +} + +func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDataMode, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> ViewController { + let initialState = DeleteAccountDataState(password: "", isLoading: false) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((DeleteAccountDataState) -> DeleteAccountDataState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var presentControllerImpl: ((ViewController) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? var replaceTopControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? + var updateCodeImpl: (() -> Void)? + + var activateInputImpl: (() -> Void)? + var dismissInputImpl: (() -> Void)? + + if case .phone = mode { + loadServerCountryCodes(accountManager: context.sharedContext.accountManager, engine: context.engine, completion: { + updateCodeImpl?() + }) + } + var updateCountryCodeImpl: ((Int32, String) -> Void)? + var proceedImpl: (() -> Void)? + let arguments = DeleteAccountDataArguments(context: context, openLink: { _ in + }, selectCountryCode: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = AuthorizationSequenceCountrySelectionController(strings: presentationData.strings, theme: presentationData.theme) + controller.completeWithCountryCode = { code, name in + updateCountryCodeImpl?(Int32(code), name) + activateInputImpl?() + } + dismissInputImpl?() + pushControllerImpl?(controller) + }, updatePassword: { password in + updateState { current in + var updated = current + updated.password = password + return updated + } + }, proceed: { + proceedImpl?() }) - let peers: Signal<[Peer], NoError> = .single([]) - + let preloadedGroupPeers = Promise<[EnginePeer]>([]) + + let peers: Signal<[EnginePeer], NoError> + switch mode { + case .peers: + peers = combineLatest( + context.engine.peers.recentPeers() + |> map { recentPeers -> [EnginePeer] in + if case let .peers(peers) = recentPeers { + return peers.map { EnginePeer($0) } + } else { + return [] + } + }, + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + ) |> map { recentPeers, accountPeer -> [EnginePeer] in + var peers: [EnginePeer] = [] + if let accountPeer = accountPeer { + peers.append(accountPeer) + } + peers.append(contentsOf: recentPeers.prefix(9)) + return peers + } + + preloadedGroupPeers.set(context.engine.peers.adminedPublicChannels(scope: .all) + |> map { peers -> [EnginePeer] in + return peers.map { EnginePeer($0) } + }) + case let .groups(preloadedPeers): + peers = .single(preloadedPeers.shuffled()) + default: + peers = .single([]) + } + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, - peers + peers, + statePromise.get() ) - |> map { presentationData, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, peers, state -> (ItemListControllerState, (ItemListNodeState, Any)) in let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) - let footerItem = DeleteAccountFooterItem(theme: presentationData.theme, title: presentationData.strings.DeleteAccount_ComeBackLater, secondaryTitle: presentationData.strings.DeleteAccount_Continue, action: { + var focusItemTag: DeleteAccountEntryTag? + var buttonTitle: String + switch mode { + case .phone: + buttonTitle = "" + case .password: + buttonTitle = "" + focusItemTag = .password + default: + buttonTitle = presentationData.strings.DeleteAccount_ComeBackLater + } + + let rightNavigationButton: ItemListNavigationButton? + if state.isLoading { + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) + } else { + rightNavigationButton = nil + } + + let footerItem = DeleteAccountFooterItem(theme: presentationData.theme, title: buttonTitle, secondaryTitle: presentationData.strings.DeleteAccount_Continue, action: { dismissImpl?() }, secondaryAction: { - let nextMode: DeleteAccountDataMode? - switch mode { - case .peers: - nextMode = .groups - case .groups: - nextMode = .messages - case .messages: - nextMode = nil - } - - if let nextMode = nextMode { - let controller = deleteAccountDataController(context: context, mode: nextMode) - replaceTopControllerImpl?(controller) - } + proceedImpl?() }) - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DeleteAccount_DeleteMyAccountTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deleteAccountDataEntries(presentationData: presentationData, mode: mode, peers: peers), style: .blocks, footerItem: footerItem) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DeleteAccount_DeleteMyAccountTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deleteAccountDataEntries(presentationData: presentationData, mode: mode, peers: peers), style: .blocks, focusItemTag: focusItemTag, footerItem: footerItem) return (controllerState, (listState, arguments)) } let controller = ItemListController(context: context, state: signal, tabBarItem: nil) + presentControllerImpl = { [weak controller] c in + controller?.present(c, in: .window(.root)) + } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } replaceTopControllerImpl = { [weak controller] c in if let navigationController = controller?.navigationController as? NavigationController { navigationController.pushViewController(c, completion: { [weak navigationController, weak controller, weak c] in @@ -184,7 +351,172 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat dismissImpl = { [weak controller] in let _ = controller?.dismiss() } - + updateCodeImpl = { [weak controller] in + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? DeleteAccountPhoneItemNode { + itemNode.updateCountryCode() + } + } + } + + activateInputImpl = { [weak controller] in + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? DeleteAccountPhoneItemNode { + itemNode.activateInput() + } + } + } + dismissInputImpl = { [weak controller] in + controller?.view.endEditing(true) + } + controller.didAppear = { firstTime in + if !firstTime { + return + } + activateInputImpl?() + } + + updateCountryCodeImpl = { [weak controller] code, name in + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? DeleteAccountPhoneItemNode { + itemNode.updateCountryCode(code: code, name: name) + } + } + } + + proceedImpl = { [weak controller] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let action: ([EnginePeer]) -> Void = { preloadedPeers in + let nextMode: DeleteAccountDataMode? + switch mode { + case .peers: + if !preloadedPeers.isEmpty { + nextMode = .groups(preloadedPeers) + } else { + nextMode = .messages + } + case .groups: + nextMode = .messages + case .messages: + nextMode = .phone + case .phone: + if let twoStepAuthData = twoStepAuthData, case .set = twoStepAuthData { + nextMode = .password + } else { + nextMode = nil + } + case .password: + nextMode = nil + } + + if let nextMode = nextMode { + let controller = deleteAccountDataController(context: context, mode: nextMode, twoStepAuthData: twoStepAuthData) + replaceTopControllerImpl?(controller) + } else { + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.DeleteAccount_ConfirmationAlertTitle, text: presentationData.strings.DeleteAccount_ConfirmationAlertText, actions: [TextAlertAction(type: .destructiveAction, title: presentationData.strings.DeleteAccount_ConfirmationAlertDelete, action: { + updateState { current in + var updated = current + updated.isLoading = true + return updated + } + + let accountId = context.account.id + let accountManager = context.sharedContext.accountManager + let _ = (context.engine.auth.deleteAccount(reason: "Manual") + |> deliverOnMainQueue).start(error: { _ in + updateState { current in + var updated = current + updated.isLoading = false + return updated + } + + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + }, completed: { + dismissImpl?() + let _ = logoutFromAccount(id: accountId, accountManager: accountManager, alreadyLoggedOutRemotely: true).start() + }) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?() + })])) + } + } + + switch mode { + case .peers: + let _ = (preloadedGroupPeers.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peers in + action(peers) + }) + case .phone: + var phoneNumber: String? + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? DeleteAccountPhoneItemNode { + phoneNumber = itemNode.phoneNumber + } + } + + if let phoneNumber = phoneNumber, phoneNumber.count > 4 { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue) + .start(next: { accountPeer in + if let accountPeer = accountPeer, case let .user(user) = accountPeer, var phone = user.phone { + if !phone.hasPrefix("+") { + phone = "+\(phone)" + } + if phone != phoneNumber { + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.DeleteAccount_InvalidPhoneNumberError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + return + } + action([]) + } + }) + } + case .password: + let state = stateValue.with { $0 } + if !state.password.isEmpty { + updateState { current in + var updated = current + updated.isLoading = true + return updated + } + + let _ = (context.engine.auth.requestTwoStepVerifiationSettings(password: state.password) + |> deliverOnMainQueue).start(error: { error in + updateState { current in + var updated = current + updated.isLoading = false + return updated + } + + let text: String + switch error { + case .limitExceeded: + text = presentationData.strings.LoginPassword_FloodError + case .invalidPassword: + text = presentationData.strings.DeleteAccount_InvalidPasswordError + default: + text = presentationData.strings.Login_UnknownError + } + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + }, completed: { + updateState { current in + var updated = current + updated.isLoading = false + return updated + } + + action([]) + }) + return + } + + default: + action([]) + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/DeleteAccountFooterItem.swift b/submodules/SettingsUI/Sources/DeleteAccountFooterItem.swift index 208bd12c34..b5b9f04b34 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountFooterItem.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountFooterItem.swift @@ -44,8 +44,9 @@ final class DeleteAccountFooterItem: ItemListControllerFooterItem { final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode { private let backgroundNode: NavigationBackgroundNode private let separatorNode: ASDisplayNode + private let clipNode: ASDisplayNode private let buttonNode: SolidRoundedButtonNode - private let secondaryButtonNode: HighlightTrackingButtonNode + private let secondaryButtonNode: HighlightableButtonNode private var validLayout: ContainerViewLayout? @@ -64,16 +65,20 @@ final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode { self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor) self.separatorNode = ASDisplayNode() + self.clipNode = ASDisplayNode() + self.clipNode.clipsToBounds = true + self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true) - self.secondaryButtonNode = HighlightTrackingButtonNode() + self.secondaryButtonNode = HighlightableButtonNode() super.init() self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) - self.addSubnode(self.buttonNode) - self.addSubnode(self.secondaryButtonNode) + self.addSubnode(self.clipNode) + self.clipNode.addSubnode(self.buttonNode) + self.clipNode.addSubnode(self.secondaryButtonNode) self.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside) @@ -121,8 +126,19 @@ final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode { let secondaryButtonSize = self.secondaryButtonNode.measure(CGSize(width: buttonWidth, height: CGFloat.greatestFiniteMagnitude)) var panelHeight: CGFloat = buttonHeight + topInset + spacing + secondaryButtonSize.height + bottomInset + + var buttonOffset: CGFloat = 0.0 let totalPanelHeight: CGFloat + + if (self.buttonNode.title?.isEmpty ?? false) { + buttonOffset = -buttonHeight - topInset + self.buttonNode.alpha = 0.0 + } else { + self.buttonNode.alpha = 1.0 + } + if let inputHeight = layout.inputHeight, inputHeight > 0.0 { + panelHeight += buttonOffset totalPanelHeight = panelHeight + insets.bottom } else { panelHeight += insets.bottom @@ -130,13 +146,15 @@ final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode { } let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) - transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + topInset), size: CGSize(width: buttonWidth, height: buttonHeight))) - - transition.updateFrame(node: self.secondaryButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - secondaryButtonSize.width) / 2.0), y: panelFrame.minY + topInset + buttonHeight + spacing), size: secondaryButtonSize)) transition.updateFrame(node: self.backgroundNode, frame: panelFrame) self.backgroundNode.update(size: panelFrame.size, transition: transition) + transition.updateFrame(node: self.clipNode, frame: panelFrame) + + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: topInset + buttonOffset), size: CGSize(width: buttonWidth, height: buttonHeight))) + transition.updateFrame(node: self.secondaryButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - secondaryButtonSize.width) / 2.0), y: topInset + buttonHeight + spacing + buttonOffset), size: secondaryButtonSize)) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel))) return panelHeight diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift index 7325104044..bc3d7f50e1 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -15,14 +15,17 @@ import PresentationDataUtils import UrlHandling import AccountUtils import PremiumUI +import PasswordSetupUI private struct DeleteAccountOptionsArguments { let changePhoneNumber: () -> Void let addAccount: () -> Void let setupPrivacy: () -> Void - let setTwoStepAuth: () -> Void + let setupTwoStepAuth: () -> Void let setPasscode: () -> Void let clearCache: () -> Void + let clearSyncedContacts: () -> Void + let deleteChats: () -> Void let contactSupport: () -> Void let deleteAccount: () -> Void } @@ -111,8 +114,8 @@ private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable { arguments.setupPrivacy() }) case let .setTwoStepAuth(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.setPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { - arguments.setTwoStepAuth() + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetTwoStepAuth, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.setupTwoStepAuth() }) case let .setPasscode(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { @@ -124,11 +127,11 @@ private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable { }) case let .clearSyncedContacts(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.clearSynced, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { - arguments.clearCache() + arguments.clearSyncedContacts() }) case let .deleteChats(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteChats, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { - arguments.clearCache() + arguments.deleteChats() }) case let .contactSupport(_, title, text): return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { @@ -168,14 +171,14 @@ private func deleteAccountOptionsEntries(presentationData: PresentationData, can return entries } -public func deleteAccountOptionsController(context: AccountContext, navigationController: NavigationController, hasTwoStepAuth: Bool) -> ViewController { +public func deleteAccountOptionsController(context: AccountContext, navigationController: NavigationController, hasTwoStepAuth: Bool, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var replaceTopControllerImpl: ((ViewController, Bool) -> Void)? var dismissImpl: (() -> Void)? let supportPeerDisposable = MetaDisposable() - + let arguments = DeleteAccountOptionsArguments(changePhoneNumber: { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.engine.account.peerId)) |> deliverOnMainQueue).start(next: { accountPeer in @@ -224,9 +227,30 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo } }) }, setupPrivacy: { + replaceTopControllerImpl?(makePrivacyAndSecurityController(context: context), false) + }, setupTwoStepAuth: { + if let data = twoStepAuthData { + switch data { + case .set: + break + case let .notSet(pendingEmail): + if pendingEmail == nil { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .intro(.init( + title: presentationData.strings.TwoFactorSetup_Intro_Title, + text: presentationData.strings.TwoFactorSetup_Intro_Text, + actionText: presentationData.strings.TwoFactorSetup_Intro_Action, + doneText: presentationData.strings.TwoFactorSetup_Done_Action + ))) - }, setTwoStepAuth: { + replaceTopControllerImpl?(controller, false) + return + } + } + } + let controller = twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: twoStepAuthData.flatMap({ Signal.single(.access(configuration: $0)) }))) + replaceTopControllerImpl?(controller, false) }, setPasscode: { let _ = passcodeOptionsAccessController(context: context, pushController: { controller in replaceTopControllerImpl?(controller, false) @@ -241,11 +265,44 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo }, clearCache: { pushControllerImpl?(storageUsageController(context: context)) dismissImpl?() + }, clearSyncedContacts: { + replaceTopControllerImpl?(dataPrivacyController(context: context), false) + }, deleteChats: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var faqUrl = presentationData.strings.DeleteAccount_DeleteMessagesURL + if faqUrl == "DeleteAccount.DeleteMessagesURL" || faqUrl.isEmpty { + faqUrl = "https://telegram.org/faq#q-can-i-delete-my-messages" + } + let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl) + + let resolvedUrlPromise = Promise() + resolvedUrlPromise.set(resolvedUrl) + + let openFaq: (Promise) -> Void = { resolvedUrl in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + presentControllerImpl?(controller, nil) + let _ = (resolvedUrl.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in + controller?.dismiss() + dismissImpl?() + + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in + pushControllerImpl?(controller) + }, dismissInput: {}, contentContext: nil) + }) + } + + openFaq(resolvedUrlPromise) }, contactSupport: { [weak navigationController] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let supportPeer = Promise() supportPeer.set(context.engine.peers.supportPeerId()) - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + var faqUrl = presentationData.strings.Settings_FAQ_URL if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { faqUrl = "https://telegram.org/faq#general" @@ -289,7 +346,7 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo }) ]), nil) }, deleteAccount: { - let controller = deleteAccountDataController(context: context, mode: .peers) + let controller = deleteAccountDataController(context: context, mode: .peers, twoStepAuthData: twoStepAuthData) replaceTopControllerImpl?(controller, true) }) @@ -337,7 +394,18 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo } }) } else { - navigationController?.replaceTopController(c, animated: true) + if c is PrivacyAndSecurityControllerImpl { + if let navigationController = navigationController { + if let existing = navigationController.viewControllers.first(where: { $0 is PrivacyAndSecurityControllerImpl }) as? ViewController { + existing.scrollToTop?() + dismissImpl?() + } else { + navigationController.replaceTopController(c, animated: true) + } + } + } else { + navigationController?.replaceTopController(c, animated: true) + } } } dismissImpl = { [weak controller] in diff --git a/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift b/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift new file mode 100644 index 0000000000..f5c1b34c52 --- /dev/null +++ b/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift @@ -0,0 +1,296 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import HorizontalPeerItem +import AccountContext +import MergeLists + +private struct PeersEntry: Comparable, Identifiable { + let index: Int + let peer: EnginePeer + let theme: PresentationTheme + let strings: PresentationStrings + + var stableId: EnginePeer.Id { + return self.peer.id + } + + static func ==(lhs: PeersEntry, rhs: PeersEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + static func <(lhs: PeersEntry, rhs: PeersEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext) -> ListViewItem { + return HorizontalPeerItem(theme: self.theme, strings: self.strings, mode: .list(compact: true), context: context, peer: self.peer, presence: nil, unreadBadge: nil, action: { _ in }, contextAction: nil, isPeerSelected: { _ in return false }, customWidth: nil) + } +} + +private struct DeleteAccountPeersItemNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let firstTime: Bool + let animated: Bool +} + +private func preparedPeersTransition(context: AccountContext, from fromEntries: [PeersEntry], to toEntries: [PeersEntry], firstTime: Bool, animated: Bool) -> DeleteAccountPeersItemNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context), directionHint: .Down) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context), directionHint: nil) } + + return DeleteAccountPeersItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, animated: animated) +} + +class DeleteAccountPeersItem: ListViewItem, ItemListItem { + var sectionId: ItemListSectionId + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peers: [EnginePeer] + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peers: [EnginePeer], sectionId: ItemListSectionId) { + self.context = context + self.theme = theme + self.strings = strings + self.peers = peers + self.sectionId = sectionId + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = DeleteAccountPeersItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? DeleteAccountPeersItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +class DeleteAccountPeersItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let dataPromise = Promise<(AccountContext, [EnginePeer], PresentationTheme, PresentationStrings)>() + private var disposable: Disposable? + + private var item: DeleteAccountPeersItem? + private var layoutParams: ListViewItemLayoutParams? + + private let listView: ListView + private var queuedTransitions: [DeleteAccountPeersItemNodeTransition] = [] + + var tag: ItemListItemTag? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.listView = ListView() + self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.listView) + + let previous: Atomic<[PeersEntry]> = Atomic(value: []) + let firstTime:Atomic = Atomic(value: true) + + self.disposable = (self.dataPromise.get() |> deliverOnMainQueue).start(next: { [weak self] data in + if let strongSelf = self { + let (context, peers, theme, strings) = data + + var entries: [PeersEntry] = [] + for peer in peers { + entries.append(PeersEntry(index: entries.count, peer: peer, theme: theme, strings: strings)) + } + + let animated = !firstTime.swap(false) + + let transition = preparedPeersTransition(context: context, from: previous.swap(entries), to: entries, firstTime: !animated, animated: animated) + strongSelf.enqueueTransition(transition) + } + }) + } + + deinit { + self.disposable?.dispose() + } + + private func enqueueTransition(_ transition: DeleteAccountPeersItemNodeTransition) { + self.queuedTransitions.append(transition) + self.dequeueTransitions() + } + + private func dequeueTransitions() { + while !self.queuedTransitions.isEmpty { + let transition = self.queuedTransitions.removeFirst() + + var options = ListViewDeleteAndInsertOptions() + if transition.firstTime { + options.insert(.PreferSynchronousResourceLoading) + options.insert(.PreferSynchronousDrawing) + options.insert(.Synchronous) + options.insert(.LowLatency) + } else if transition.animated { + options.insert(.AnimateInsertion) + } + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { _ in }) + } + } + + func asyncLayout() -> (_ item: DeleteAccountPeersItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + } + print(themeUpdated) + + let contentSize: CGSize + var insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + contentSize = CGSize(width: params.width, height: 109.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.dataPromise.set(.single((item.context, item.peers, item.theme, item.strings))) + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.addSubnode(strongSelf.maskNode) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = params.leftInset + 16.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + let listInsets = UIEdgeInsets(top: params.leftInset, left: 0.0, bottom: params.rightInset, right: 0.0) + strongSelf.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: params.width) + strongSelf.listView.position = CGPoint(x: params.width / 2.0, y: contentSize.height / 2.0) + strongSelf.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: params.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + strongSelf.dequeueTransitions() + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift b/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift new file mode 100644 index 0000000000..a36fbca9a2 --- /dev/null +++ b/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift @@ -0,0 +1,413 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import PhoneInputNode +import CountrySelectionUI +import CoreTelephony + +private func generateCountryButtonBackground(color: UIColor, strokeColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 56, height: 44.0 + 6.0), rotatedContext: { size, context in + let arrowSize: CGFloat = 6.0 + let lineWidth = UIScreenPixel + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize))) + context.move(to: CGPoint(x: size.width, y: size.height - arrowSize)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize)) + context.closePath() + context.fillPath() + + context.setStrokeColor(strokeColor.cgColor) + context.setLineWidth(lineWidth) + + context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: 15.0, y: size.height - arrowSize - lineWidth / 2.0)) + context.strokePath() + + context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0)) + context.strokePath() + })?.stretchableImage(withLeftCapWidth: 55, topCapHeight: 1) +} + +private func generateCountryButtonHighlightedBackground(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 56.0, height: 44.0 + 6.0), rotatedContext: { size, context in + let arrowSize: CGFloat = 6.0 + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize))) + context.move(to: CGPoint(x: size.width, y: size.height - arrowSize)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height)) + context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize)) + context.closePath() + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 55, topCapHeight: 2) +} + +private func generatePhoneInputBackground(color: UIColor, strokeColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 82.0, height: 44.0), rotatedContext: { size, context in + let lineWidth = UIScreenPixel + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(strokeColor.cgColor) + context.setLineWidth(lineWidth) + context.move(to: CGPoint(x: 0.0, y: size.height - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: size.height - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: 0.0)) + context.strokePath() + })?.stretchableImage(withLeftCapWidth: 81, topCapHeight: 2) +} + + +class DeleteAccountPhoneItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings + let value: (Int32?, String?, String) + let sectionId: ItemListSectionId + let selectCountryCode: () -> Void + let updated: (Int) -> Void + + init(theme: PresentationTheme, strings: PresentationStrings, value: (Int32?, String?, String), sectionId: ItemListSectionId, selectCountryCode: @escaping () -> Void, updated: @escaping (Int) -> Void) { + self.theme = theme + self.strings = strings + self.value = value + self.sectionId = sectionId + self.selectCountryCode = selectCountryCode + self.updated = updated + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = DeleteAccountPhoneItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? DeleteAccountPhoneItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +class DeleteAccountPhoneItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let countryButton: ASButtonNode + private let phoneBackground: ASImageNode + private let phoneInputNode: PhoneInputNode + + private var item: DeleteAccountPhoneItem? + private var layoutParams: ListViewItemLayoutParams? + + var preferredCountryIdForCode: [String: String] = [:] + + var tag: ItemListItemTag? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.countryButton = ASButtonNode() + + self.phoneBackground = ASImageNode() + self.phoneBackground.displaysAsynchronously = false + self.phoneBackground.displayWithoutProcessing = true + self.phoneBackground.isLayerBacked = true + + self.phoneInputNode = PhoneInputNode(fontSize: 17.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.phoneBackground) + self.addSubnode(self.countryButton) + self.addSubnode(self.phoneInputNode) + + self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 4.0, right: 0.0) + self.countryButton.contentHorizontalAlignment = .left + + self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside) + + let processNumberChange: (String) -> Bool = { [weak self] number in + guard let strongSelf = self, let item = strongSelf.item else { + return false + } + if let (country, _) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(number, preferredCountries: strongSelf.preferredCountryIdForCode) { + let flagString = emojiFlagForISOCountryCode(country.id) + let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(country.id, strings: item.strings) ?? country.name + strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(17.0), with: item.theme.list.itemPrimaryTextColor, for: []) + + let maskFont = Font.with(size: 20.0, design: .regular, traits: [.monospacedNumbers]) + if let mask = AuthorizationSequenceCountrySelectionController.lookupPatternByNumber(number, preferredCountries: strongSelf.preferredCountryIdForCode).flatMap({ NSAttributedString(string: $0, font: maskFont, textColor: item.theme.list.itemPlaceholderTextColor) }) { + strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = nil + strongSelf.phoneInputNode.mask = mask + } else { + strongSelf.phoneInputNode.mask = nil + strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: item.strings.Login_PhonePlaceholder, font: Font.regular(20.0), textColor: item.theme.list.itemPlaceholderTextColor) + } + return true + } else { + return false + } + } + + self.phoneInputNode.numberTextUpdated = { [weak self] number in + if let strongSelf = self { + let _ = processNumberChange(strongSelf.phoneInputNode.number) + } + } + + self.phoneInputNode.countryCodeUpdated = { [weak self] code, name in + if let strongSelf = self, let item = strongSelf.item { + if let name = name { + strongSelf.preferredCountryIdForCode[code] = name + } + + if processNumberChange(strongSelf.phoneInputNode.number) { + } else if let code = Int(code), let name = name, let countryName = countryCodeAndIdToName[CountryCodeAndId(code: code, id: name)] { + let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(name, strings: item.strings) ?? countryName + strongSelf.countryButton.setTitle(localizedName, with: Font.regular(17.0), with: item.theme.list.itemPrimaryTextColor, for: []) + } else if let code = Int(code), let (_, countryName) = countryCodeToIdAndName[code] { + strongSelf.countryButton.setTitle(countryName, with: Font.regular(17.0), with: item.theme.list.itemPrimaryTextColor, for: []) + } else { + strongSelf.countryButton.setTitle(item.strings.Login_CountryCode, with: Font.regular(17.0), with: item.theme.list.itemPrimaryTextColor, for: []) + } + } + } + + self.phoneInputNode.customFormatter = { number in + if let (_, code) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(number, preferredCountries: [:]) { + return code.code + } else { + return nil + } + } + + var countryId: String? = nil + let networkInfo = CTTelephonyNetworkInfo() + if let carrier = networkInfo.subscriberCellularProvider { + countryId = carrier.isoCountryCode + } + + if countryId == nil { + countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String + } + + var countryCodeAndId: (Int32, String) = (1, "US") + + if let countryId = countryId { + let normalizedId = countryId.uppercased() + for (code, idAndName) in countryCodeToIdAndName { + if idAndName.0 == normalizedId { + countryCodeAndId = (Int32(code), idAndName.0.uppercased()) + break + } + } + } + + self.phoneInputNode.number = "+\(countryCodeAndId.0)" + } + + @objc private func countryPressed() { + if let item = self.item { + item.selectCountryCode() + } + } + + var phoneNumber: String { + return self.phoneInputNode.number + } + + func updateCountryCode() { + self.phoneInputNode.codeAndNumber = self.phoneInputNode.codeAndNumber + } + + func updateCountryCode(code: Int32, name: String) { + self.phoneInputNode.codeAndNumber = (code, name, self.phoneInputNode.codeAndNumber.2) + } + + func activateInput() { + self.phoneInputNode.numberField.textField.becomeFirstResponder() + } + + func animateError() { + self.phoneInputNode.countryCodeField.layer.addShakeAnimation() + self.phoneInputNode.numberField.layer.addShakeAnimation() + } + + func asyncLayout() -> (_ item: DeleteAccountPhoneItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + var updatedCountryButtonBackground: UIImage? + var updatedCountryButtonHighlightedBackground: UIImage? + var updatedPhoneBackground: UIImage? + + if currentItem?.theme !== item.theme { + updatedCountryButtonBackground = generateCountryButtonBackground(color: item.theme.list.itemBlocksBackgroundColor, strokeColor: item.theme.list.itemBlocksSeparatorColor) + updatedCountryButtonHighlightedBackground = generateCountryButtonHighlightedBackground(color: item.theme.list.itemHighlightedBackgroundColor) + updatedPhoneBackground = generatePhoneInputBackground(color: item.theme.list.itemBlocksBackgroundColor, strokeColor: item.theme.list.itemBlocksSeparatorColor) + } + + let contentSize: CGSize + var insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let countryButtonHeight: CGFloat = 44.0 + let inputFieldsHeight: CGFloat = 44.0 + + contentSize = CGSize(width: params.width, height: countryButtonHeight + inputFieldsHeight) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.addSubnode(strongSelf.maskNode) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = params.leftInset + 16.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + if let updatedCountryButtonBackground = updatedCountryButtonBackground { + strongSelf.countryButton.setBackgroundImage(updatedCountryButtonBackground, for: []) + } + if let updatedCountryButtonHighlightedBackground = updatedCountryButtonHighlightedBackground { + strongSelf.countryButton.setBackgroundImage(updatedCountryButtonHighlightedBackground, for: .highlighted) + } + if let updatedPhoneBackground = updatedPhoneBackground { + strongSelf.phoneBackground.image = updatedPhoneBackground + } + + strongSelf.phoneInputNode.countryCodeField.textField.textColor = item.theme.list.itemPrimaryTextColor + strongSelf.phoneInputNode.countryCodeField.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance + strongSelf.phoneInputNode.countryCodeField.textField.tintColor = item.theme.list.itemAccentColor + strongSelf.phoneInputNode.numberField.textField.textColor = item.theme.list.itemPrimaryTextColor + strongSelf.phoneInputNode.numberField.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance + strongSelf.phoneInputNode.numberField.textField.tintColor = item.theme.list.itemAccentColor + + strongSelf.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: params.leftInset + 15.0, bottom: 4.0, right: 0.0) + + strongSelf.countryButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: 44.0 + 6.0)) + strongSelf.phoneBackground.frame = CGRect(origin: CGPoint(x: 0.0, y: 44.0), size: CGSize(width: params.width, height: 44.0)) + + let countryCodeFrame = CGRect(origin: CGPoint(x: 11.0, y: 44.0), size: CGSize(width: 67.0, height: 44.0)) + let numberFrame = CGRect(origin: CGPoint(x: 92.0, y: 44.0), size: CGSize(width: layout.size.width - 70.0 - 8.0, height: 44.0)) + let placeholderFrame = numberFrame.offsetBy(dx: 0.0, dy: 8.0) + + let phoneInputFrame = countryCodeFrame.union(numberFrame) + + strongSelf.phoneInputNode.frame = phoneInputFrame + strongSelf.phoneInputNode.countryCodeField.frame = countryCodeFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY) + strongSelf.phoneInputNode.numberField.frame = numberFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY) + strongSelf.phoneInputNode.placeholderNode.frame = placeholderFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index f594a23db5..7a070c170a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -482,6 +482,10 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD return entries } +class PrivacyAndSecurityControllerImpl: ItemListController { + +} + public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, updatedBlockedPeers: ((BlockedPeersContext?) -> Void)? = nil, updatedHasTwoStepAuth: ((Bool) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil, webSessionsContext: WebSessionsContext? = nil, blockedPeersContext: BlockedPeersContext? = nil, hasTwoStepAuth: Bool? = nil) -> ViewController { let statePromise = ValuePromise(PrivacyAndSecurityControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: PrivacyAndSecurityControllerState()) @@ -492,6 +496,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting var pushControllerImpl: ((ViewController, Bool) -> Void)? var replaceTopControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)? + var getNavigationControllerImpl: (() -> NavigationController?)? let actionsDisposable = DisposableSet() @@ -822,12 +827,26 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting 6 * 30 * 24 * 60 * 60, 365 * 24 * 60 * 60 ] - let timeoutItems: [ActionSheetItem] = timeoutValues.map { value in + var timeoutItems: [ActionSheetItem] = timeoutValues.map { value in return ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: value), action: { dismissAction() timeoutAction(value) }) } + timeoutItems.append(ActionSheetButtonItem(title: presentationData.strings.PrivacySettings_DeleteAccountNow, color: .destructive, action: { + dismissAction() + + guard let navigationController = getNavigationControllerImpl?() else { + return + } + + let _ = (combineLatest(twoStepAuth.get(), twoStepAuthDataValue.get()) + |> take(1) + |> deliverOnMainQueue).start(next: { hasTwoStepAuth, twoStepAuthData in + let optionsController = deleteAccountOptionsController(context: context, navigationController: navigationController, hasTwoStepAuth: hasTwoStepAuth ?? false, twoStepAuthData: twoStepAuthData) + pushControllerImpl?(optionsController, true) + }) + })) controller.setItemGroups([ ActionSheetItemGroup(items: timeoutItems), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) @@ -886,7 +905,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting actionsDisposable.dispose() } - let controller = ItemListController(context: context, state: signal) + let controller = PrivacyAndSecurityControllerImpl(context: context, state: signal) pushControllerImpl = { [weak controller] c, animated in (controller?.navigationController as? NavigationController)?.pushViewController(c, animated: animated) } @@ -896,7 +915,10 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } - + getNavigationControllerImpl = { [weak controller] in + return (controller?.navigationController as? NavigationController) + } + controller.didAppear = { _ in updateHasTwoStepAuth() } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index 4ab4a52ad1..2d53114931 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -206,7 +206,7 @@ private func twoStepVerificationUnlockSettingsControllerEntries(presentationData if let pendingEmail = pendingEmail { entries.append(.pendingEmailConfirmInfo(presentationData.theme, presentationData.strings.TwoStepAuth_SetupPendingEmail(pendingEmail.email.pattern).string)) entries.append(.pendingEmailConfirmCode(presentationData.theme, presentationData.strings, presentationData.strings.TwoStepAuth_RecoveryCode, state.emailCode)) - entries.append(.pendingEmailInfo(presentationData.theme, "[" + presentationData.strings.TwoStepAuth_ConfirmationAbort + "]()")) + entries.append(.pendingEmailInfo(presentationData.theme, "[" + presentationData.strings.TwoStepAuth_ConfirmationAbort + "]()")) /*entries.append(.pendingEmailInfo(presentationData.theme, presentationData.strings.TwoStepAuth_ConfirmationText + "\n\n\(pendingEmailAndValue.pendingEmail.pattern)\n\n[" + presentationData.strings.TwoStepAuth_ConfirmationAbort + "]()"))*/ } else { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 6731dbcd62..5910683d1f 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -91,6 +91,7 @@ public struct PresentationResourcesSettings { public static let changePhoneNumber = renderIcon(name: "Settings/Menu/ChangePhoneNumber") public static let deleteAddAccount = renderIcon(name: "Settings/Menu/DeleteAddAccount") + public static let deleteSetTwoStepAuth = renderIcon(name: "Settings/Menu/DeleteTwoStepAuth") public static let deleteSetPasscode = renderIcon(name: "Settings/Menu/FaceId") public static let deleteChats = renderIcon(name: "Settings/Menu/DeleteChats") public static let clearSynced = renderIcon(name: "Settings/Menu/ClearSynced") diff --git a/submodules/TelegramUI/Images.xcassets/Components/BadgeTEst.imageset/AppBadge@3x.png b/submodules/TelegramUI/Images.xcassets/Components/AppBadge.imageset/AppBadge@3x.png similarity index 100% rename from submodules/TelegramUI/Images.xcassets/Components/BadgeTEst.imageset/AppBadge@3x.png rename to submodules/TelegramUI/Images.xcassets/Components/AppBadge.imageset/AppBadge@3x.png diff --git a/submodules/TelegramUI/Images.xcassets/Components/BadgeTEst.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/AppBadge.imageset/Contents.json similarity index 100% rename from submodules/TelegramUI/Images.xcassets/Components/BadgeTEst.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Components/AppBadge.imageset/Contents.json diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteTwoStepAuth.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteTwoStepAuth.imageset/Contents.json new file mode 100644 index 0000000000..bfbeb098c2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteTwoStepAuth.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteTwoStepAuth.imageset/Icon.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteTwoStepAuth.imageset/Icon.pdf new file mode 100644 index 0000000000..d771d5afaf --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteTwoStepAuth.imageset/Icon.pdf @@ -0,0 +1,114 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.345098 0.337255 0.839216 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +-1.000000 -0.000000 -0.000000 1.000000 25.000000 6.000000 cm +1.000000 1.000000 1.000000 scn +6.650000 19.000000 m +2.945000 19.000000 0.000000 16.055000 0.000000 12.350000 c +0.000000 8.645000 2.945000 5.700001 6.650000 5.700001 c +7.500165 5.700001 8.310955 5.861581 9.054688 6.145313 c +10.450001 4.750000 l +12.350000 4.750000 l +12.350000 2.850000 l +14.250000 2.850000 l +14.250000 0.950001 l +14.903126 0.296875 l +15.093125 0.106874 15.300938 0.000000 15.585938 0.000000 c +18.049999 0.000000 l +18.619999 0.000000 19.000000 0.380001 19.000000 0.950001 c +19.000000 3.414062 l +19.000000 3.699062 18.893126 3.906875 18.703125 4.096874 c +12.854687 9.945312 l +13.138419 10.689045 13.299999 11.499835 13.299999 12.350000 c +13.299999 16.055000 10.355000 19.000000 6.650000 19.000000 c +h +5.225000 16.150000 m +6.555000 16.150000 7.600000 15.105000 7.600000 13.775000 c +7.600000 12.445000 6.555000 11.400000 5.225000 11.400000 c +3.895000 11.400000 2.850000 12.445000 2.850000 13.775000 c +2.850000 15.105000 3.895000 16.150000 5.225000 16.150000 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1987 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002077 00000 n +0000002100 00000 n +0000002273 00000 n +0000002347 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2406 +%%EOF \ No newline at end of file