Add account deletion

This commit is contained in:
Ilya Laktyushin 2022-06-30 18:55:19 +03:00
parent 411b8682c1
commit 16d6010385
48 changed files with 3222 additions and 65 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -868,7 +868,6 @@
"MessageTimer.Years_2" = "%@ years";
"MessageTimer.Years_3_10" = "%@ years";
"MessageTimer.Years_any" = "%@ years";
"MessageTimer.Months_many" = "%@ years";
"MessageTimer.ShortSeconds_1" = "%@s";
"MessageTimer.ShortSeconds_2" = "%@s";
@ -7725,7 +7724,6 @@ Sorry for the inconvenience.";
"Premium.Restore.Success" = "Done";
"Premium.Restore.ErrorUnknown" = "An error occurred. Please try again.";
"Settings.Premium" = "Telegram Premium";
"Settings.AddAnotherAccount.PremiumHelp" = "You can add up to four accounts with different phone numbers.";
@ -7735,3 +7733,67 @@ Sorry for the inconvenience.";
"Appearance.AppIconPremium" = "Premium";
"Appearance.AppIconBlack" = "Black";
"Appearance.AppIconTurbo" = "Turbo";
"PrivacySettings.DeleteAccountNow" = "Delete Account Now";
"DeleteAccount.AlternativeOptionsTitle" = "Alternative Options";
"DeleteAccount.Options.ChangePhoneNumberTitle" = "Change Phone Number";
"DeleteAccount.Options.ChangePhoneNumberText" = "Move your contacts, chats and media to a new number.";
"DeleteAccount.Options.AddAccountTitle" = "Add Another Account";
"DeleteAccount.Options.AddAccountText" = "You can use up to 3 accounts in one app at the same time.";
"DeleteAccount.Options.AddAccountPremiumText" = "You can use up to 4 accounts in one app at the same time.";
"DeleteAccount.Options.ChangePrivacyTitle" = "Change Your Privacy Settings";
"DeleteAccount.Options.ChangePrivacyText" = "Choose who exactly can see which of your info.";
"DeleteAccount.Options.SetTwoStepAuthTitle" = "Enable Two-Step Verification";
"DeleteAccount.Options.SetTwoStepAuthText" = "Set a password that will be required each time you log in.";
"DeleteAccount.Options.SetPasscodeTitle" = "Set a Passcode";
"DeleteAccount.Options.SetPasscodeText" = "Lock the app with a passcode so that others can't open it.";
"DeleteAccount.Options.ClearCacheTitle" = "Clear Cache";
"DeleteAccount.Options.ClearCacheText" = "Free up disk space on your device; your media will stay in the cloud.";
"DeleteAccount.Options.ClearSyncedContactsTitle" = "Clear Synced Contacts";
"DeleteAccount.Options.ClearSyncedContactsText" = "Remove any unnecessary contacts you may have synced.";
"DeleteAccount.Options.DeleteChatsTitle" = "Quickly Delete Your Chats";
"DeleteAccount.Options.DeleteChatsText" = "Learn how to remove any info you dont need in a few taps.";
"DeleteAccount.Options.ContactSupportTitle" = "Contact Support";
"DeleteAccount.Options.ContactSupportText" = "Tell us about any issues; deleting account doesn't usually help.";
"DeleteAccount.DeleteMyAccountTitle" = "Delete My Account";
"DeleteAccount.DeleteMyAccount" = "Delete My Account";
"DeleteAccount.SavedMessages" = "Saved";
"DeleteAccount.ComeBackLater" = "Come Back Later";
"DeleteAccount.Continue" = "Continue";
"DeleteAccount.CloudStorageTitle" = "Your Free Cloud Storage";
"DeleteAccount.CloudStorageText" = "You will lose access to all your Saved Messages as well as all messages, media and files from your chats.";
"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.";
"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.";
"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!";
"DeleteAccount.ConfirmationAlertDelete" = "Delete My Account";
"DeleteAccount.Success" = "The account has been successfully deleted.";

View File

@ -5,6 +5,7 @@ import TelegramUIPreferences
import AccountContext
public let maximumNumberOfAccounts = 3
public let maximumPremiumNumberOfAccounts = 4
public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool = false) -> Signal<((AccountContext, EnginePeer)?, [(AccountContext, EnginePeer, Int32)]), NoError> {
let sharedContext = context.sharedContext

View File

@ -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)

View File

@ -312,6 +312,15 @@ public class CheckLayer: CALayer {
}
}
public override init() {
self.theme = CheckNodeTheme(backgroundColor: .white, strokeColor: .blue, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false)
self.content = .check
super.init()
self.isOpaque = false
}
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) {
self.theme = theme
self.content = content

View File

@ -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)
}

View File

@ -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

View File

@ -15,14 +15,16 @@ import TextFormat
public class InviteLinkHeaderItem: ListViewItem, ItemListItem {
public let context: AccountContext
public let theme: PresentationTheme
public let title: String?
public let text: String
public let animationName: String
public let sectionId: ItemListSectionId
public let linkAction: ((ItemListTextItemLinkAction) -> Void)?
public init(context: AccountContext, theme: PresentationTheme, text: String, animationName: String, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) {
public init(context: AccountContext, theme: PresentationTheme, title: String? = nil, text: String, animationName: String, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) {
self.context = context
self.theme = theme
self.title = title
self.text = text
self.animationName = animationName
self.sectionId = sectionId
@ -66,10 +68,12 @@ public class InviteLinkHeaderItem: ListViewItem, ItemListItem {
}
}
private let titleFont = Font.regular(13.0)
private let titleFont = Font.semibold(17.0)
private let textFont = Font.regular(14.0)
class InviteLinkHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private let textNode: TextNode
private var animationNode: AnimatedStickerNode
private var item: InviteLinkHeaderItem?
@ -80,11 +84,17 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
self.animationNode = DefaultAnimatedStickerNodeImpl()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.animationNode)
}
@ -100,18 +110,33 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
return { item, params, neighbors in
let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 124.0
let leftInset: CGFloat = 24.0 + params.leftInset
let iconSize: CGSize
if params.width > params.availableHeight && params.width > 320.0 {
iconSize = CGSize(width: 140.0, height: 140.0)
} else {
iconSize = CGSize(width: 124.0, height: 124.0)
}
let topInset: CGFloat = iconSize.height - 4.0
let spacing: CGFloat = 5.0
let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in
let attributedTitle = NSAttributedString(string: item.title ?? "", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}))
let (titleLayout, titleApply) = makeTitleLayout(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 contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
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 - 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 {
contentSize.height += titleLayout.size.height + spacing
}
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
@ -119,18 +144,25 @@ 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)
var origin: CGFloat = topInset + 8.0
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: origin), size: titleLayout.size)
if titleLayout.size.height > 0.0 {
origin += titleLayout.size.height + spacing
}
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size)
}
})
}
@ -150,9 +182,9 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let titleFrame = self.titleNode.frame
if let item = self.item, titleFrame.contains(location) {
if let (_, attributes) = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
let textFrame = self.textNode.frame
if let item = self.item, textFrame.contains(location) {
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url))
}

View File

@ -1147,7 +1147,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
let _ = combineLatest(
queue: Queue.mainQueue(),
adminedPublicChannels.get() |> filter { $0 != nil } |> take(1),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)

View File

@ -23,7 +23,7 @@ final class IncreaseLimitFooterItem: ItemListControllerFooterItem {
func isEqual(to: ItemListControllerFooterItem) -> Bool {
if let item = to as? IncreaseLimitFooterItem {
return self.theme === item.theme && self.title == item.title
return self.theme === item.theme && self.title == item.title && self.colorful == item.colorful
} else {
return false
}

View File

@ -14,10 +14,6 @@ public final class MatrixView: MTKView, MTKViewDelegate, PhoneDemoDecorationView
private var displayLink: CADisplayLink?
// private var metalLayer: CAMetalLayer {
// return self.layer as! CAMetalLayer
// }
private let symbolTexture: MTLTexture
private let randomTexture: MTLTexture

View File

@ -410,7 +410,7 @@ class PremiumStarComponent: Component {
if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) {
if let particleSystem = particles.particleSystems?.first {
particleSystem.particleColorVariation = SCNVector4(0.15, 0.2, 0.35, 0.3)
particleSystem.particleColorVariation = SCNVector4(0.15, 0.2, 0.15, 0.3)
particleSystem.speedFactor = 2.0
particleSystem.particleVelocity = 2.2
particleSystem.birthRate = 4.0

View File

@ -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

View File

@ -101,6 +101,8 @@ swift_library(
"//submodules/ListMessageItem:ListMessageItem",
"//submodules/PaymentMethodUI:PaymentMethodUI",
"//submodules/PremiumUI:PremiumUI",
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
],
visibility = [
"//visibility:public",

View File

@ -286,5 +286,4 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode {
@objc func countryPressed() {
self.selectCountryCode?()
}
}

View File

@ -0,0 +1,567 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import UrlHandling
import InviteLinksUI
import CountrySelectionUI
import PhoneInputNode
import UndoUI
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, [EnginePeer])
case phone(PresentationTheme, PresentationStrings)
case password(PresentationTheme, String)
case info(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .header:
return DeleteAccountDataSection.header.rawValue
case .peers, .info, .phone, .password:
return DeleteAccountDataSection.main.rawValue
}
}
var stableId: Int32 {
switch self {
case .header:
return 0
case .peers:
return 1
case .info:
return 2
case .phone:
return 3
case .password:
return 4
}
}
static func == (lhs: DeleteAccountDataEntry, rhs: DeleteAccountDataEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsAnimation, lhsTitle, lhsText):
if case let .header(rhsTheme, rhsAnimation, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsAnimation == rhsAnimation, lhsTitle == rhsTitle, lhsText == rhsText {
return true
} else {
return false
}
case let .peers(lhsTheme, lhsPeers):
if case let .peers(rhsTheme, rhsPeers) = rhs, lhsTheme === rhsTheme, lhsPeers == rhsPeers {
return true
} else {
return false
}
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} 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
}
}
}
static func <(lhs: DeleteAccountDataEntry, rhs: DeleteAccountDataEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DeleteAccountDataArguments
switch self {
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 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: [EnginePeer]) -> [DeleteAccountDataEntry] {
var entries: [DeleteAccountDataEntry] = []
let headerTitle: String
let headerText: String
let headerAnimation: String
switch mode {
case .peers:
headerAnimation = "Delete1"
headerTitle = presentationData.strings.DeleteAccount_CloudStorageTitle
headerText = presentationData.strings.DeleteAccount_CloudStorageText
case .groups:
headerAnimation = "Delete2"
headerTitle = presentationData.strings.DeleteAccount_GroupsAndChannelsTitle
headerText = presentationData.strings.DeleteAccount_GroupsAndChannelsText
case .messages:
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))
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
}
enum DeleteAccountDataMode {
case peers
case groups([EnginePeer])
case messages
case phone
case password
}
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 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 cancelImpl = {
dismissImpl?()
switch mode {
case .peers:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_cloud_cancel")
case .groups:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_groups_cancel")
case .messages:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_messages_cancel")
case .phone:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_phone_cancel")
case .password:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_2fa_cancel")
}
}
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
peers,
statePromise.get()
)
|> map { presentationData, peers, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
cancelImpl()
})
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: {
cancelImpl()
}, secondaryAction: {
proceedImpl?()
})
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
if let navigationController = navigationController {
let controllers = navigationController.viewControllers.filter { $0 !== controller }
c?.navigationPresentation = .modal
navigationController.setViewControllers(controllers, animated: false)
}
})
}
}
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], String?) -> Void = { preloadedPeers, password 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 {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_confirmation_show")
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.DeleteAccount_ConfirmationAlertTitle, text: presentationData.strings.DeleteAccount_ConfirmationAlertText, actions: [TextAlertAction(type: .destructiveAction, title: presentationData.strings.DeleteAccount_ConfirmationAlertDelete, action: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.final")
invokeAppLogEventsSynchronization(postbox: context.account.postbox)
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", password: password)
|> 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 presentGlobalController = context.sharedContext.presentGlobalController
let _ = logoutFromAccount(id: accountId, accountManager: accountManager, alreadyLoggedOutRemotely: true).start(completed: {
Queue.mainQueue().after(0.1) {
presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}
})
})
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_confirmation_cancel")
dismissImpl?()
})]))
}
}
switch mode {
case .peers:
let _ = (preloadedGroupPeers.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peers in
action(peers, nil)
})
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([], nil)
}
})
}
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([], state.password)
})
return
}
default:
action([], nil)
}
}
switch mode {
case .peers:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_cloud_show")
case .groups:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_groups_show")
case .messages:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_messages_show")
case .phone:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_phone_show")
case .password:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_2fa_show")
}
return controller
}

View File

@ -0,0 +1,170 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import SolidRoundedButtonNode
import AppBundle
final class DeleteAccountFooterItem: ItemListControllerFooterItem {
let theme: PresentationTheme
let title: String
let secondaryTitle: String
let action: () -> Void
let secondaryAction: () -> Void
init(theme: PresentationTheme, title: String, secondaryTitle: String, action: @escaping () -> Void, secondaryAction: @escaping () -> Void) {
self.theme = theme
self.title = title
self.secondaryTitle = secondaryTitle
self.action = action
self.secondaryAction = secondaryAction
}
func isEqual(to: ItemListControllerFooterItem) -> Bool {
if let item = to as? DeleteAccountFooterItem {
return self.theme === item.theme && self.title == item.title && self.secondaryTitle == item.secondaryTitle
} else {
return false
}
}
func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode {
if let current = current as? DeleteAccountFooterItemNode {
current.item = self
return current
} else {
return DeleteAccountFooterItemNode(item: self)
}
}
}
final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode {
private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode
private let clipNode: ASDisplayNode
private let buttonNode: SolidRoundedButtonNode
private let secondaryButtonNode: HighlightableButtonNode
private var validLayout: ContainerViewLayout?
var item: DeleteAccountFooterItem {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
}
}
}
init(item: DeleteAccountFooterItem) {
self.item = item
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 = HighlightableButtonNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.clipNode)
self.clipNode.addSubnode(self.buttonNode)
self.clipNode.addSubnode(self.secondaryButtonNode)
self.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside)
self.updateItem()
}
@objc private func secondaryButtonPressed() {
self.item.secondaryAction()
}
private func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor
let backgroundColor = self.item.theme.list.itemCheckColors.fillColor
let textColor = self.item.theme.list.itemCheckColors.foregroundColor
self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: backgroundColor, foregroundColor: textColor), animated: false)
self.buttonNode.title = self.item.title
self.buttonNode.pressed = { [weak self] in
self?.item.action()
}
self.secondaryButtonNode.setTitle(self.item.secondaryTitle, with: Font.regular(17.0), with: self.item.theme.list.itemAccentColor, for: .normal)
}
override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.backgroundNode, alpha: alpha)
transition.updateAlpha(node: self.separatorNode, alpha: alpha)
}
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = layout
let buttonInset: CGFloat = 16.0
let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
let topInset: CGFloat = 9.0
let bottomInset: CGFloat = 23.0
let spacing: CGFloat = 23.0
let insets = layout.insets(options: [.input])
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
totalPanelHeight = panelHeight
}
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.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
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.backgroundNode.frame.contains(point) {
return true
} else {
return false
}
}
}

View File

@ -0,0 +1,441 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AlertUI
import PresentationDataUtils
import UrlHandling
import AccountUtils
import PremiumUI
import PasswordSetupUI
private struct DeleteAccountOptionsArguments {
let changePhoneNumber: () -> Void
let addAccount: () -> Void
let setupPrivacy: () -> Void
let setupTwoStepAuth: () -> Void
let setPasscode: () -> Void
let clearCache: () -> Void
let clearSyncedContacts: () -> Void
let deleteChats: () -> Void
let contactSupport: () -> Void
let deleteAccount: () -> Void
}
private enum DeleteAccountOptionsSection: Int32 {
case add
case privacy
case remove
case support
case delete
}
private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable {
case changePhoneNumber(PresentationTheme, String, String)
case addAccount(PresentationTheme, String, String)
case changePrivacy(PresentationTheme, String, String)
case setTwoStepAuth(PresentationTheme, String, String)
case setPasscode(PresentationTheme, String, String)
case clearCache(PresentationTheme, String, String)
case clearSyncedContacts(PresentationTheme, String, String)
case deleteChats(PresentationTheme, String, String)
case contactSupport(PresentationTheme, String, String)
case deleteAccount(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .changePhoneNumber, .addAccount:
return DeleteAccountOptionsSection.add.rawValue
case .changePrivacy, .setTwoStepAuth, .setPasscode:
return DeleteAccountOptionsSection.privacy.rawValue
case .clearCache, .clearSyncedContacts, .deleteChats:
return DeleteAccountOptionsSection.remove.rawValue
case .contactSupport:
return DeleteAccountOptionsSection.support.rawValue
case .deleteAccount:
return DeleteAccountOptionsSection.delete.rawValue
}
}
var stableId: Int32 {
switch self {
case .changePhoneNumber:
return 0
case .addAccount:
return 1
case .changePrivacy:
return 2
case .setTwoStepAuth:
return 3
case .setPasscode:
return 4
case .clearCache:
return 5
case .clearSyncedContacts:
return 6
case .deleteChats:
return 7
case .contactSupport:
return 8
case .deleteAccount:
return 9
}
}
static func <(lhs: DeleteAccountOptionsEntry, rhs: DeleteAccountOptionsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DeleteAccountOptionsArguments
switch self {
case let .changePhoneNumber(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.changePhoneNumber()
})
case let .addAccount(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteAddAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.addAccount()
})
case let .changePrivacy(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.security, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.setupPrivacy()
})
case let .setTwoStepAuth(_, title, text):
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: {
arguments.setPasscode()
})
case let .clearCache(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.dataAndStorage, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.clearCache()
})
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.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.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: {
arguments.contactSupport()
})
case let .deleteAccount(_, title):
return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteAccount()
})
}
}
}
private func deleteAccountOptionsEntries(presentationData: PresentationData, canAddAccounts: Bool, hasTwoStepAuth: Bool, hasPasscode: Bool) -> [DeleteAccountOptionsEntry] {
var entries: [DeleteAccountOptionsEntry] = []
entries.append(.changePhoneNumber(presentationData.theme, presentationData.strings.DeleteAccount_Options_ChangePhoneNumberTitle, presentationData.strings.DeleteAccount_Options_ChangePhoneNumberText))
if canAddAccounts {
entries.append(.addAccount(presentationData.theme, presentationData.strings.DeleteAccount_Options_AddAccountTitle, presentationData.strings.DeleteAccount_Options_AddAccountText))
}
entries.append(.changePrivacy(presentationData.theme, presentationData.strings.DeleteAccount_Options_ChangePrivacyTitle, presentationData.strings.DeleteAccount_Options_ChangePrivacyText))
if !hasTwoStepAuth {
entries.append(.setTwoStepAuth(presentationData.theme, presentationData.strings.DeleteAccount_Options_SetTwoStepAuthTitle, presentationData.strings.DeleteAccount_Options_SetTwoStepAuthText))
}
if !hasPasscode {
entries.append(.setPasscode(presentationData.theme, presentationData.strings.DeleteAccount_Options_SetPasscodeTitle, presentationData.strings.DeleteAccount_Options_SetPasscodeText))
}
entries.append(.clearCache(presentationData.theme, presentationData.strings.DeleteAccount_Options_ClearCacheTitle, presentationData.strings.DeleteAccount_Options_ClearCacheText))
entries.append(.clearSyncedContacts(presentationData.theme, presentationData.strings.DeleteAccount_Options_ClearSyncedContactsTitle, presentationData.strings.DeleteAccount_Options_ClearSyncedContactsText))
entries.append(.deleteChats(presentationData.theme, presentationData.strings.DeleteAccount_Options_DeleteChatsTitle, presentationData.strings.DeleteAccount_Options_DeleteChatsText))
entries.append(.contactSupport(presentationData.theme, presentationData.strings.DeleteAccount_Options_ContactSupportTitle, presentationData.strings.DeleteAccount_Options_ContactSupportText))
entries.append(.deleteAccount(presentationData.theme, presentationData.strings.DeleteAccount_DeleteMyAccount))
return entries
}
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: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_phone_change_tap")
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.engine.account.peerId))
|> deliverOnMainQueue).start(next: { accountPeer in
guard let accountPeer = accountPeer, case let .user(user) = accountPeer else {
return
}
let introController = PrivacyIntroController(context: context, mode: .changePhoneNumber(user.phone ?? ""), proceedAction: {
replaceTopControllerImpl?(ChangePhoneNumberController(context: context), false)
})
pushControllerImpl?(introController)
dismissImpl?()
})
}, addAccount: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_add_account_tap")
let _ = (activeAccountsAndPeers(context: context)
|> take(1)
|> deliverOnMainQueue
).start(next: { accountAndPeer, accountsAndPeers in
var maximumAvailableAccounts: Int = 3
if accountAndPeer?.1.isPremium == true && !context.account.testingEnvironment {
maximumAvailableAccounts = 4
}
var count: Int = 1
for (accountContext, peer, _) in accountsAndPeers {
if !accountContext.account.testingEnvironment {
if peer.isPremium {
maximumAvailableAccounts = 4
}
count += 1
}
}
if count >= maximumAvailableAccounts {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: {
let controller = PremiumIntroScreen(context: context, source: .accounts)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
} else {
context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment)
dismissImpl?()
}
})
}, setupPrivacy: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_privacy_tap")
replaceTopControllerImpl?(makePrivacyAndSecurityController(context: context), false)
}, setupTwoStepAuth: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_2fa_tap")
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
)))
replaceTopControllerImpl?(controller, false)
return
}
}
}
let controller = twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: twoStepAuthData.flatMap({ Signal<TwoStepVerificationUnlockSettingsControllerData, NoError>.single(.access(configuration: $0)) })))
replaceTopControllerImpl?(controller, false)
}, setPasscode: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_passcode_tap")
let _ = passcodeOptionsAccessController(context: context, pushController: { controller in
replaceTopControllerImpl?(controller, false)
}, completion: { _ in
replaceTopControllerImpl?(passcodeOptionsController(context: context), false)
}).start(next: { controller in
if let controller = controller {
pushControllerImpl?(controller)
}
})
dismissImpl?()
}, clearCache: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_clear_cache_tap")
pushControllerImpl?(storageUsageController(context: context))
dismissImpl?()
}, clearSyncedContacts: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_clear_contacts_tap")
replaceTopControllerImpl?(dataPrivacyController(context: context), false)
}, deleteChats: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_delete_chats_tap")
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<ResolvedUrl>()
resolvedUrlPromise.set(resolvedUrl)
let openFaq: (Promise<ResolvedUrl>) -> 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
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_support_tap")
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let supportPeer = Promise<PeerId?>()
supportPeer.set(context.engine.peers.supportPeerId())
var faqUrl = presentationData.strings.Settings_FAQ_URL
if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty {
faqUrl = "https://telegram.org/faq#general"
}
let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl)
let resolvedUrlPromise = Promise<ResolvedUrl>()
resolvedUrlPromise.set(resolvedUrl)
let openFaq: (Promise<ResolvedUrl>) -> 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)
})
}
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: {
openFaq(resolvedUrlPromise)
dismissImpl?()
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
supportPeerDisposable.set((supportPeer.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId, let navigationController = navigationController {
dismissImpl?()
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(id: peerId)))
}
}))
})
])
alertController.dismissed = {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_support_cancel")
}
presentControllerImpl?(alertController, nil)
}, deleteAccount: {
let controller = deleteAccountDataController(context: context, mode: .peers, twoStepAuthData: twoStepAuthData)
replaceTopControllerImpl?(controller, true)
})
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
context.sharedContext.accountManager.accessChallengeData(),
activeAccountsAndPeers(context: context)
)
|> map { presentationData, accessChallengeData, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var hasPasscode = false
switch accessChallengeData.data {
case .numericalPassword, .plaintextPassword:
hasPasscode = true
default:
break
}
let canAddAccounts = accountsAndPeers.1.count + 1 < maximumNumberOfAccounts
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DeleteAccount_AlternativeOptionsTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deleteAccountOptionsEntries(presentationData: presentationData, canAddAccounts: canAddAccounts, hasTwoStepAuth: hasTwoStepAuth, hasPasscode: hasPasscode), style: .blocks)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal, tabBarItem: nil)
controller.navigationPresentation = .modal
pushControllerImpl = { [weak navigationController] value in
navigationController?.pushViewController(value, animated: false)
}
presentControllerImpl = { [weak controller] value, arguments in
controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
replaceTopControllerImpl = { [weak navigationController] c, complex in
if complex {
navigationController?.pushViewController(c, completion: { [weak navigationController, weak controller, weak c] in
if let navigationController = navigationController {
let controllers = navigationController.viewControllers.filter { $0 !== controller }
c?.navigationPresentation = .modal
navigationController.setViewControllers(controllers, animated: false)
}
})
} else {
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
let _ = controller?.dismiss()
}
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_show")
return controller
}

View File

@ -0,0 +1,288 @@
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<Void, NoError>?, (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<Bool> = 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) {
return { item, params, neighbors in
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)
}
}

View File

@ -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<Void, NoError>?, (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)
}
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -424,7 +424,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
}
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
if premiumConfiguration.isPremiumDisabled {
if premiumConfiguration.isPremiumDisabled || context.account.testingEnvironment {
appIcons = appIcons.filter { !$0.isPremium }
}

View File

@ -317,6 +317,8 @@ class TabBarNode: ASDisplayNode {
}
}
var reduceMotion: Bool = false
var selectedIndex: Int? {
didSet {
if self.selectedIndex != oldValue {
@ -574,12 +576,14 @@ class TabBarNode: ASDisplayNode {
node.contentWidth = max(contentWidth, imageContentWidth)
node.isSelected = true
ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTransformScale(node: node.ringImageNode, scale: 1.0, delay: 0.1)
node.imageNode.layer.animateScale(from: 1.0, to: 0.87, duration: 0.1, removeOnCompletion: false, completion: { [weak node] _ in
node?.imageNode.layer.animateScale(from: 0.87, to: 1.0, duration: 0.14, removeOnCompletion: false, completion: { [weak node] _ in
node?.imageNode.layer.removeAllAnimations()
if !self.reduceMotion && item.item.ringSelection {
ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTransformScale(node: node.ringImageNode, scale: 1.0, delay: 0.1)
node.imageNode.layer.animateScale(from: 1.0, to: 0.87, duration: 0.1, removeOnCompletion: false, completion: { [weak node] _ in
node?.imageNode.layer.animateScale(from: 0.87, to: 1.0, duration: 0.14, removeOnCompletion: false, completion: { [weak node] _ in
node?.imageNode.layer.removeAllAnimations()
})
})
})
}
} else {
let (textImage, contentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)

View File

@ -156,11 +156,13 @@ public extension Api.functions.account {
}
}
public extension Api.functions.account {
static func deleteAccount(reason: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
static func deleteAccount(flags: Int32, reason: String, password: Api.InputCheckPasswordSRP?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(1099779595)
buffer.appendInt32(-1564422284)
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(reason, buffer: buffer, boxed: false)
return (FunctionDescription(name: "account.deleteAccount", parameters: [("reason", String(describing: reason))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
if Int(flags) & Int(1 << 0) != 0 {password!.serialize(buffer, true)}
return (FunctionDescription(name: "account.deleteAccount", parameters: [("flags", String(describing: flags)), ("reason", String(describing: reason)), ("password", String(describing: password))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {

View File

@ -494,7 +494,7 @@ public enum AccountResetError {
}
public func performAccountReset(account: UnauthorizedAccount) -> Signal<Void, AccountResetError> {
return account.network.request(Api.functions.account.deleteAccount(reason: ""))
return account.network.request(Api.functions.account.deleteAccount(flags: 0, reason: "", password: nil))
|> map { _ -> Int32? in return nil }
|> `catch` { error -> Signal<Int32?, AccountResetError> in
if error.errorDescription.hasPrefix("2FA_CONFIRM_WAIT_") {

View File

@ -90,12 +90,41 @@ public extension TelegramEngine {
return _internal_updateTwoStepVerificationPassword(network: self.account.network, currentPassword: currentPassword, updatedPassword: updatedPassword)
}
public func deleteAccount(reason: String) -> Signal<Never, DeleteAccountError> {
return self.account.network.request(Api.functions.account.deleteAccount(reason: reason))
|> mapError { _ -> DeleteAccountError in
return .generic
public func deleteAccount(reason: String, password: String?) -> Signal<Never, DeleteAccountError> {
let network = self.account.network
let passwordSignal: Signal<Api.InputCheckPasswordSRP?, DeleteAccountError>
if let password = password {
passwordSignal = _internal_twoStepAuthData(network)
|> mapError { _ -> DeleteAccountError in
return .generic
}
|> mapToSignal { authData -> Signal<Api.InputCheckPasswordSRP?, DeleteAccountError> in
if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData {
guard let kdfResult = passwordKDF(encryptionProvider: network.encryptionProvider, password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else {
return .fail(.generic)
}
return .single(.inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1)))
} else {
return .single(nil)
}
}
} else {
passwordSignal = .single(nil)
}
return passwordSignal
|> mapToSignal { password -> Signal<Never, DeleteAccountError> in
var flags: Int32 = 0
if let _ = password {
flags |= (1 << 0)
}
return self.account.network.request(Api.functions.account.deleteAccount(flags: flags, reason: reason, password: password))
|> mapError { _ -> DeleteAccountError in
return .generic
}
|> ignoreValues
}
|> ignoreValues
}
public func updateTwoStepVerificationEmail(currentPassword: String, updatedEmail: String) -> Signal<UpdateTwoStepVerificationPasswordResult, UpdateTwoStepVerificationPasswordError> {

View File

@ -90,5 +90,11 @@ public struct PresentationResourcesSettings {
public static let clearCache = renderIcon(name: "Settings/Menu/ClearCache")
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")
public static let websites = renderIcon(name: "Settings/Menu/Websites")
}

View File

@ -1,12 +1,15 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_tb_settings.pdf"
"filename" : "ic_tb_settings.pdf",
"idiom" : "universal"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "gift.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,512 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 24.000000 24.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 7.992188 7.880981 cm
0.000000 0.000000 0.000000 scn
3.800998 1.276080 m
1.993796 0.168980 l
1.805881 0.053862 1.560225 0.112875 1.445107 0.300790 c
1.388883 0.392569 1.372121 0.503168 1.398624 0.607484 c
1.678378 1.708605 l
1.779364 2.106091 2.051364 2.438358 2.421074 2.615862 c
4.392641 3.562446 l
4.484557 3.606576 4.523294 3.716863 4.479164 3.808778 c
4.443426 3.883215 4.362605 3.924868 4.281243 3.910783 c
2.086636 3.530841 l
1.640524 3.453608 1.183040 3.576797 0.836031 3.867601 c
0.142737 4.448601 l
-0.026168 4.590147 -0.048346 4.841818 0.093201 5.010722 c
0.162044 5.092872 0.261042 5.143870 0.367897 5.152233 c
2.486118 5.318005 l
2.635765 5.329716 2.766179 5.424419 2.823627 5.563095 c
3.640796 7.535693 l
3.725137 7.739288 3.958555 7.835961 4.162150 7.751620 c
4.259909 7.711123 4.337579 7.633452 4.378077 7.535693 c
5.195247 5.563095 l
5.252695 5.424419 5.383109 5.329716 5.532755 5.318005 c
7.662615 5.151322 l
7.882316 5.134129 8.046480 4.942087 8.029286 4.722386 c
8.021015 4.616698 7.971026 4.518645 7.890352 4.449868 c
6.265997 3.065068 l
6.151649 2.967583 6.101762 2.814124 6.136929 2.668034 c
6.636304 0.593517 l
6.687879 0.379265 6.556002 0.163769 6.341750 0.112194 c
6.238800 0.087412 6.130221 0.104568 6.039927 0.159883 c
4.217875 1.276080 l
4.089963 1.354439 3.928910 1.354439 3.800998 1.276080 c
h
f*
n
Q
endstream
endobj
2 0 obj
1398
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 24.000000 24.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 6.000000 5.500000 cm
0.000000 0.000000 0.000000 scn
0.000000 6.000000 m
0.000000 9.313708 2.686292 12.000000 6.000000 12.000000 c
6.000000 12.000000 l
9.313708 12.000000 12.000000 9.313708 12.000000 6.000000 c
12.000000 6.000000 l
12.000000 2.686292 9.313708 0.000000 6.000000 0.000000 c
6.000000 0.000000 l
2.686292 0.000000 0.000000 2.686292 0.000000 6.000000 c
0.000000 6.000000 l
h
f
n
Q
endstream
endobj
4 0 obj
459
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.000000 2.669434 cm
0.000000 0.000000 0.000000 scn
0.665000 9.330566 m
0.665000 9.697836 0.367269 9.995566 0.000000 9.995566 c
-0.367269 9.995566 -0.665000 9.697836 -0.665000 9.330566 c
0.665000 9.330566 l
h
14.665000 9.330566 m
14.665000 9.697836 14.367270 9.995566 14.000000 9.995566 c
13.632730 9.995566 13.335000 9.697836 13.335000 9.330566 c
14.665000 9.330566 l
h
1.092019 1.548553 m
1.393923 2.141073 l
1.092019 1.548553 l
h
12.907981 1.548553 m
12.606077 2.141073 l
12.907981 1.548553 l
h
13.782013 2.422585 m
13.189494 2.724489 l
13.782013 2.422585 l
h
10.800000 1.995566 m
3.200000 1.995566 l
3.200000 0.665566 l
10.800000 0.665566 l
10.800000 1.995566 l
h
0.665000 4.530566 m
0.665000 9.330566 l
-0.665000 9.330566 l
-0.665000 4.530566 l
0.665000 4.530566 l
h
13.335000 9.330566 m
13.335000 4.530566 l
14.665000 4.530566 l
14.665000 9.330566 l
13.335000 9.330566 l
h
3.200000 1.995566 m
2.628974 1.995566 2.240699 1.996084 1.940556 2.020606 c
1.648176 2.044495 1.498463 2.087807 1.393923 2.141073 c
0.790115 0.956034 l
1.113398 0.791313 1.457623 0.725632 1.832252 0.695024 c
2.199117 0.665050 2.650920 0.665566 3.200000 0.665566 c
3.200000 1.995566 l
h
-0.665000 4.530566 m
-0.665000 3.981487 -0.665517 3.529683 -0.635543 3.162818 c
-0.604935 2.788190 -0.539253 2.443964 -0.374532 2.120682 c
0.810506 2.724489 l
0.757240 2.829030 0.713928 2.978743 0.690040 3.271122 c
0.665517 3.571266 0.665000 3.959541 0.665000 4.530566 c
-0.665000 4.530566 l
h
1.393923 2.141073 m
1.142726 2.269063 0.938497 2.473293 0.810506 2.724489 c
-0.374532 2.120682 l
-0.119030 1.619230 0.288663 1.211536 0.790115 0.956034 c
1.393923 2.141073 l
h
10.800000 0.665566 m
11.349079 0.665566 11.800883 0.665050 12.167748 0.695024 c
12.542377 0.725632 12.886601 0.791313 13.209885 0.956034 c
12.606077 2.141073 l
12.501536 2.087807 12.351824 2.044495 12.059443 2.020606 c
11.759300 1.996084 11.371026 1.995566 10.800000 1.995566 c
10.800000 0.665566 l
h
13.335000 4.530566 m
13.335000 3.959541 13.334483 3.571266 13.309960 3.271122 c
13.286072 2.978743 13.242760 2.829030 13.189494 2.724489 c
14.374533 2.120682 l
14.539253 2.443964 14.604935 2.788189 14.635543 3.162818 c
14.665517 3.529683 14.665000 3.981487 14.665000 4.530566 c
13.335000 4.530566 l
h
13.209885 0.956034 m
13.711337 1.211536 14.119030 1.619230 14.374533 2.120682 c
13.189494 2.724489 l
13.061502 2.473293 12.857274 2.269063 12.606077 2.141073 c
13.209885 0.956034 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 4.000000 11.169189 cm
0.000000 0.000000 0.000000 scn
2.000000 0.665811 m
2.367269 0.665811 2.665000 0.963541 2.665000 1.330811 c
2.665000 1.698080 2.367269 1.995811 2.000000 1.995811 c
2.000000 0.665811 l
h
14.000000 1.995811 m
13.632730 1.995811 13.335000 1.698080 13.335000 1.330811 c
13.335000 0.963541 13.632730 0.665811 14.000000 0.665811 c
14.000000 1.995811 l
h
0.115213 2.017745 m
0.716366 2.302069 l
0.115213 2.017745 l
h
0.686934 1.446023 m
0.402610 0.844871 l
0.686934 1.446023 l
h
15.884788 2.017745 m
16.485941 1.733420 l
15.884788 2.017745 l
h
15.313066 1.446023 m
15.597390 0.844871 l
15.313066 1.446023 l
h
14.855110 6.138789 m
15.139435 6.739942 l
14.855110 6.138789 l
h
15.807979 5.185921 m
15.206825 4.901597 l
15.807979 5.185921 l
h
3.125000 5.665811 m
12.875000 5.665811 l
12.875000 6.995811 l
3.125000 6.995811 l
3.125000 5.665811 l
h
1.875000 0.665811 m
2.000000 0.665811 l
2.000000 1.995811 l
1.875000 1.995811 l
1.875000 0.665811 l
h
14.125000 1.995811 m
14.000000 1.995811 l
14.000000 0.665811 l
14.125000 0.665811 l
14.125000 1.995811 l
h
-0.665000 3.205811 m
-0.665000 2.901255 -0.665455 2.635354 -0.648653 2.416179 c
-0.631310 2.189949 -0.592755 1.959262 -0.485940 1.733420 c
0.716366 2.302069 l
0.707968 2.319824 0.688916 2.368348 0.677456 2.517840 c
0.665455 2.674387 0.665000 2.880721 0.665000 3.205811 c
-0.665000 3.205811 l
h
1.875000 1.995811 m
1.549910 1.995811 1.343576 1.996265 1.187029 2.008266 c
1.037537 2.019727 0.989013 2.038779 0.971258 2.047176 c
0.402610 0.844871 l
0.628452 0.738055 0.859138 0.699501 1.085368 0.682158 c
1.304543 0.665356 1.570444 0.665811 1.875000 0.665811 c
1.875000 1.995811 l
h
-0.485940 1.733420 m
-0.301460 1.343369 0.012559 1.029351 0.402610 0.844871 c
0.971258 2.047176 l
0.859367 2.100097 0.769286 2.190177 0.716366 2.302069 c
-0.485940 1.733420 l
h
15.335000 3.205811 m
15.335000 2.880721 15.334545 2.674387 15.322544 2.517839 c
15.311084 2.368348 15.292032 2.319824 15.283634 2.302069 c
16.485941 1.733420 l
16.592754 1.959262 16.631310 2.189949 16.648653 2.416179 c
16.665455 2.635354 16.665001 2.901255 16.665001 3.205811 c
15.335000 3.205811 l
h
14.125000 0.665811 m
14.429556 0.665811 14.695457 0.665356 14.914632 0.682158 c
15.140862 0.699501 15.371549 0.738055 15.597390 0.844871 c
15.028742 2.047176 l
15.010986 2.038779 14.962463 2.019727 14.812971 2.008266 c
14.656424 1.996265 14.450090 1.995811 14.125000 1.995811 c
14.125000 0.665811 l
h
15.283634 2.302069 m
15.230714 2.190177 15.140634 2.100097 15.028742 2.047176 c
15.597390 0.844871 l
15.987441 1.029351 16.301460 1.343369 16.485941 1.733420 c
15.283634 2.302069 l
h
12.875000 5.665811 m
13.409972 5.665811 13.773717 5.665356 14.055506 5.643754 c
14.330238 5.622692 14.471831 5.584438 14.570786 5.537636 c
15.139435 6.739942 l
14.832394 6.885161 14.508637 6.942918 14.157166 6.969862 c
13.812751 6.996265 13.389438 6.995811 12.875000 6.995811 c
12.875000 5.665811 l
h
16.665001 3.205811 m
16.665001 3.720248 16.665455 4.143561 16.639051 4.487977 c
16.612108 4.839448 16.554352 5.163204 16.409132 5.470245 c
15.206825 4.901597 l
15.253628 4.802642 15.291882 4.661048 15.312943 4.386316 c
15.334545 4.104527 15.335000 3.740782 15.335000 3.205811 c
16.665001 3.205811 l
h
14.570786 5.537636 m
14.849992 5.405582 15.074771 5.180802 15.206825 4.901597 c
16.409132 5.470245 l
16.145517 6.027610 15.696799 6.476328 15.139435 6.739942 c
14.570786 5.537636 l
h
3.125000 6.995811 m
2.610562 6.995811 2.187250 6.996265 1.842834 6.969862 c
1.491363 6.942918 1.167606 6.885161 0.860566 6.739942 c
1.429214 5.537636 l
1.528168 5.584438 1.669762 5.622692 1.944495 5.643754 c
2.226283 5.665356 2.590028 5.665811 3.125000 5.665811 c
3.125000 6.995811 l
h
0.665000 3.205811 m
0.665000 3.740783 0.665455 4.104527 0.687057 4.386316 c
0.708118 4.661048 0.746372 4.802642 0.793174 4.901597 c
-0.409131 5.470245 l
-0.554351 5.163204 -0.612108 4.839448 -0.639052 4.487977 c
-0.665455 4.143561 -0.665000 3.720249 -0.665000 3.205811 c
0.665000 3.205811 l
h
0.860566 6.739942 m
0.303200 6.476328 -0.145517 6.027610 -0.409131 5.470245 c
0.793174 4.901597 l
0.925229 5.180802 1.150008 5.405582 1.429214 5.537636 c
0.860566 6.739942 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 8.000000 16.169922 cm
0.000000 0.000000 0.000000 scn
4.000000 1.330078 m
4.000000 0.665078 l
4.665000 0.665078 l
4.665000 1.330078 l
4.000000 1.330078 l
h
3.335000 3.330078 m
3.335000 1.330078 l
4.665000 1.330078 l
4.665000 3.330078 l
3.335000 3.330078 l
h
4.000000 1.995078 m
2.000000 1.995078 l
2.000000 0.665078 l
4.000000 0.665078 l
4.000000 1.995078 l
h
2.000000 1.995078 m
1.262700 1.995078 0.665000 2.592778 0.665000 3.330078 c
-0.665000 3.330078 l
-0.665000 1.858239 0.528161 0.665078 2.000000 0.665078 c
2.000000 1.995078 l
h
2.000000 4.665078 m
2.737300 4.665078 3.335000 4.067378 3.335000 3.330078 c
4.665000 3.330078 l
4.665000 4.801917 3.471839 5.995078 2.000000 5.995078 c
2.000000 4.665078 l
h
2.000000 5.995078 m
0.528161 5.995078 -0.665000 4.801917 -0.665000 3.330078 c
0.665000 3.330078 l
0.665000 4.067378 1.262700 4.665078 2.000000 4.665078 c
2.000000 5.995078 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 12.000000 16.169922 cm
0.000000 0.000000 0.000000 scn
0.000000 1.330078 m
-0.665000 1.330078 l
-0.665000 0.665078 l
0.000000 0.665078 l
0.000000 1.330078 l
h
2.000000 1.995078 m
0.000000 1.995078 l
0.000000 0.665078 l
2.000000 0.665078 l
2.000000 1.995078 l
h
0.665000 1.330078 m
0.665000 3.330078 l
-0.665000 3.330078 l
-0.665000 1.330078 l
0.665000 1.330078 l
h
3.335000 3.330078 m
3.335000 2.592778 2.737300 1.995078 2.000000 1.995078 c
2.000000 0.665078 l
3.471839 0.665078 4.665000 1.858239 4.665000 3.330078 c
3.335000 3.330078 l
h
2.000000 4.665078 m
2.737300 4.665078 3.335000 4.067378 3.335000 3.330078 c
4.665000 3.330078 l
4.665000 4.801917 3.471839 5.995078 2.000000 5.995078 c
2.000000 4.665078 l
h
2.000000 5.995078 m
0.528161 5.995078 -0.665000 4.801917 -0.665000 3.330078 c
0.665000 3.330078 l
0.665000 4.067378 1.262700 4.665078 2.000000 4.665078 c
2.000000 5.995078 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 12.000000 3.169922 cm
0.000000 0.000000 0.000000 scn
0.665000 3.330078 m
0.665000 3.697347 0.367269 3.995078 0.000000 3.995078 c
-0.367269 3.995078 -0.665000 3.697347 -0.665000 3.330078 c
0.665000 3.330078 l
h
-0.665000 1.330078 m
-0.665000 0.962809 -0.367269 0.665078 0.000000 0.665078 c
0.367269 0.665078 0.665000 0.962809 0.665000 1.330078 c
-0.665000 1.330078 l
h
-0.665000 3.330078 m
-0.665000 1.330078 l
0.665000 1.330078 l
0.665000 3.330078 l
-0.665000 3.330078 l
h
f
n
Q
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
9085
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000001656 00000 n
0000001679 00000 n
0000002386 00000 n
0000002408 00000 n
0000002706 00000 n
0000011847 00000 n
0000011870 00000 n
0000012043 00000 n
0000012117 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
12177
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "clearsynced.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,136 @@
%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
1.000000 0.584314 0.000000 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 2.652100 8.000000 cm
1.000000 1.000000 1.000000 scn
15.347914 11.500000 m
15.347914 9.843145 14.004768 8.500000 12.347914 8.500000 c
10.691060 8.500000 9.347914 9.843145 9.347914 11.500000 c
9.347914 13.156855 10.691060 14.500000 12.347914 14.500000 c
14.004768 14.500000 15.347914 13.156855 15.347914 11.500000 c
h
16.206648 5.096860 m
17.131365 4.487454 17.732334 3.680024 18.122902 2.887923 c
18.464052 2.196046 18.353537 1.522406 17.970762 1.000000 c
17.531406 0.400373 16.733355 -0.000002 15.847914 -0.000002 c
8.847914 -0.000002 l
7.962472 -0.000002 7.164421 0.400373 6.725066 1.000000 c
6.701973 1.031517 6.679871 1.063584 6.658800 1.096172 c
6.330600 1.603753 6.252357 2.237787 6.572926 2.887924 c
6.987937 3.729597 7.640509 4.588578 8.666627 5.208897 c
9.571874 5.756145 10.767847 6.117645 12.347914 6.117645 c
13.927979 6.117645 15.123951 5.756145 16.029198 5.208899 c
16.089634 5.172363 16.148775 5.134999 16.206648 5.096860 c
h
5.347910 6.000000 m
6.038868 6.000000 6.650654 5.923974 7.192287 5.790671 c
6.346507 5.094717 5.770174 4.267296 5.380054 3.476105 c
4.957832 2.619810 4.950275 1.751801 5.228493 1.000000 c
2.054037 1.000000 l
0.595407 1.000000 -0.530585 2.298071 0.261390 3.522970 c
1.064612 4.765267 2.563349 6.000000 5.347910 6.000000 c
h
19.315775 3.476104 m
19.737995 2.619809 19.745552 1.751801 19.467335 1.000000 c
22.641785 1.000000 l
24.100414 1.000000 25.226404 2.298071 24.434431 3.522971 c
23.631208 4.765267 22.132471 6.000000 19.347910 6.000000 c
18.656954 6.000000 18.045172 5.923975 17.503540 5.790672 c
18.349321 5.094717 18.925655 4.267296 19.315775 3.476104 c
h
7.847914 10.500000 m
7.847914 9.119288 6.728626 8.000000 5.347914 8.000000 c
3.967202 8.000000 2.847914 9.119288 2.847914 10.500000 c
2.847914 11.880713 3.967202 13.000000 5.347914 13.000000 c
6.728626 13.000000 7.847914 11.880713 7.847914 10.500000 c
h
21.847914 10.500000 m
21.847914 9.119288 20.728626 8.000000 19.347914 8.000000 c
17.967201 8.000000 16.847914 9.119288 16.847914 10.500000 c
16.847914 11.880713 17.967201 13.000000 19.347914 13.000000 c
20.728626 13.000000 21.847914 11.880713 21.847914 10.500000 c
h
f*
n
Q
endstream
endobj
3 0 obj
3113
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
0000003203 00000 n
0000003226 00000 n
0000003399 00000 n
0000003473 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
3532
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "addaccount.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,102 @@
%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
1.000000 0.584314 0.000000 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 6.730286 6.000000 cm
1.000000 1.000000 1.000000 scn
8.269731 10.500000 m
10.478869 10.500000 12.269731 12.290861 12.269731 14.500000 c
12.269731 16.709139 10.478869 18.500000 8.269731 18.500000 c
6.060592 18.500000 4.269731 16.709139 4.269731 14.500000 c
4.269731 12.290861 6.060592 10.500000 8.269731 10.500000 c
h
16.238016 3.829396 m
15.145898 5.884616 12.897719 8.000000 8.269734 8.000000 c
3.641746 8.000000 1.393565 5.884617 0.301445 3.829397 c
-0.735194 1.878584 1.060591 0.000000 3.269730 0.000000 c
13.269731 0.000000 l
15.478869 0.000000 17.274656 1.878582 16.238016 3.829396 c
h
f*
n
Q
endstream
endobj
3 0 obj
1580
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
0000001670 00000 n
0000001693 00000 n
0000001866 00000 n
0000001940 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1999
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "deletechats.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,175 @@
%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
1.000000 0.176471 0.333333 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 6.834961 5.000000 cm
1.000000 1.000000 1.000000 scn
7.038761 21.165039 m
7.064992 21.165039 l
9.264992 21.165039 l
9.291224 21.165039 l
9.291246 21.165039 l
9.688916 21.165049 10.026937 21.165058 10.304341 21.142393 c
10.595594 21.118597 10.878077 21.066540 11.147882 20.929068 c
11.555252 20.721500 11.886456 20.390299 12.094021 19.982927 c
12.231494 19.713121 12.283551 19.430639 12.307348 19.139387 c
12.330013 18.861979 12.330004 18.523952 12.329992 18.126278 c
12.329992 18.100037 l
12.329992 17.665022 l
15.665000 17.665022 l
16.032269 17.665022 16.330000 17.367292 16.330000 17.000023 c
16.330000 16.632753 16.032269 16.335022 15.665000 16.335022 c
0.665000 16.335022 l
0.297731 16.335022 0.000000 16.632753 0.000000 17.000023 c
0.000000 17.367292 0.297731 17.665022 0.665000 17.665022 c
3.999992 17.665022 l
3.999992 18.100037 l
3.999992 18.126268 l
3.999981 18.523949 3.999972 18.861977 4.022637 19.139387 c
4.046433 19.430639 4.098491 19.713121 4.235963 19.982927 c
4.443529 20.390299 4.774732 20.721500 5.182103 20.929068 c
5.451908 21.066540 5.734391 21.118597 6.025643 21.142393 c
6.303048 21.165058 6.641069 21.165049 7.038738 21.165039 c
7.038761 21.165039 l
h
10.999992 18.100037 m
10.999992 17.665022 l
5.329992 17.665022 l
5.329992 18.100037 l
5.329992 18.531050 5.330510 18.814316 5.348220 19.031082 c
5.365296 19.240086 5.394984 19.328056 5.421002 19.379120 c
5.501056 19.536236 5.628795 19.663975 5.785910 19.744028 c
5.836973 19.770046 5.924943 19.799734 6.133947 19.816811 c
6.350715 19.834520 6.633980 19.835037 7.064992 19.835037 c
9.264992 19.835037 l
9.696005 19.835037 9.979270 19.834520 10.196037 19.816811 c
10.405041 19.799734 10.493011 19.770046 10.544075 19.744028 c
10.701189 19.663975 10.828928 19.536236 10.908983 19.379120 c
10.935000 19.328056 10.964688 19.240086 10.981765 19.031082 c
10.999475 18.814316 10.999992 18.531050 10.999992 18.100037 c
h
1.866287 4.480732 m
1.278763 13.293592 l
1.239206 13.886940 1.219428 14.183613 1.322701 14.412016 c
1.413458 14.612740 1.567953 14.777878 1.762195 14.881785 c
1.983222 15.000023 2.280554 15.000023 2.875219 15.000023 c
13.454782 15.000023 l
14.049447 15.000023 14.346780 15.000023 14.567807 14.881785 c
14.762049 14.777878 14.916544 14.612740 15.007301 14.412016 c
15.110574 14.183613 15.090796 13.886940 15.051239 13.293592 c
14.463715 4.480732 l
14.358499 2.902485 14.305890 2.113361 13.965018 1.515018 c
13.664913 0.988235 13.212244 0.564739 12.666664 0.300339 c
12.046970 0.000023 11.256096 0.000023 9.674346 0.000023 c
6.655656 0.000023 l
5.073906 0.000023 4.283031 0.000023 3.663338 0.300339 c
3.117758 0.564739 2.665089 0.988235 2.364984 1.515018 c
2.024112 2.113361 1.971503 2.902485 1.866287 4.480732 c
h
5.420216 12.030214 m
5.403539 12.397105 5.092596 12.681009 4.725706 12.664333 c
4.358815 12.647655 4.074911 12.336713 4.091588 11.969823 c
4.500678 2.969837 l
4.517354 2.602947 4.828298 2.319042 5.195188 2.335720 c
5.562078 2.352396 5.845983 2.663338 5.829306 3.030230 c
5.420216 12.030214 l
h
11.604276 12.664333 m
11.971167 12.647655 12.255072 12.336713 12.238394 11.969823 c
11.829304 2.969837 l
11.812627 2.602947 11.501684 2.319042 11.134793 2.335720 c
10.767903 2.352396 10.483999 2.663338 10.500675 3.030230 c
10.909766 12.030214 l
10.926443 12.397105 11.237386 12.681009 11.604276 12.664333 c
h
8.164989 12.665019 m
8.532259 12.665019 8.829989 12.367289 8.829989 12.000019 c
8.829989 3.000034 l
8.829989 2.632763 8.532259 2.335033 8.164989 2.335033 c
7.797720 2.335033 7.499990 2.632763 7.499990 3.000034 c
7.499990 12.000019 l
7.499990 12.367289 7.797720 12.665019 8.164989 12.665019 c
h
f*
n
Q
endstream
endobj
3 0 obj
4588
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
0000004678 00000 n
0000004701 00000 n
0000004874 00000 n
0000004948 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
5007
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Icon.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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

View File

@ -486,7 +486,7 @@ final class AuthorizedApplicationContext {
}
let accountId = strongSelf.context.account.id
let accountManager = strongSelf.context.sharedContext.accountManager
let _ = (strongSelf.context.engine.auth.deleteAccount(reason: "GDPR")
let _ = (strongSelf.context.engine.auth.deleteAccount(reason: "GDPR", password: nil)
|> deliverOnMainQueue).start(error: { _ in
guard let strongSelf = self else {
return

View File

@ -7972,8 +7972,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen {
icon = UIImage(bundleImageName: "Chat List/Tabs/IconSettings")
}
let tabBarItem: Signal<(String, UIImage?, UIImage?, String?, Bool), NoError> = combineLatest(queue: .mainQueue(), self.context.sharedContext.presentationData, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), getServerProvidedSuggestions(account: self.context.account), accountTabBarAvatar, accountTabBarAvatarBadge)
|> map { presentationData, notificationsAuthorizationStatus, notificationsWarningSuppressed, suggestions, accountTabBarAvatar, accountTabBarAvatarBadge -> (String, UIImage?, UIImage?, String?, Bool) in
let tabBarItem: Signal<(String, UIImage?, UIImage?, String?, Bool, Bool), NoError> = combineLatest(queue: .mainQueue(), self.context.sharedContext.presentationData, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), getServerProvidedSuggestions(account: self.context.account), accountTabBarAvatar, accountTabBarAvatarBadge)
|> map { presentationData, notificationsAuthorizationStatus, notificationsWarningSuppressed, suggestions, accountTabBarAvatar, accountTabBarAvatarBadge -> (String, UIImage?, UIImage?, String?, Bool, Bool) in
let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed)
let phoneNumberWarning = suggestions.contains(.validatePhoneNumber)
let passwordWarning = suggestions.contains(.validatePassword)
@ -7981,15 +7981,15 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen {
if accountTabBarAvatarBadge > 0 {
otherAccountsBadge = compactNumericCountString(Int(accountTabBarAvatarBadge), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
return (presentationData.strings.Settings_Title, accountTabBarAvatar?.0 ?? icon, accountTabBarAvatar?.1 ?? icon, notificationsWarning || phoneNumberWarning || passwordWarning ? "!" : otherAccountsBadge, accountTabBarAvatar != nil || presentationData.reduceMotion)
return (presentationData.strings.Settings_Title, accountTabBarAvatar?.0 ?? icon, accountTabBarAvatar?.1 ?? icon, notificationsWarning || phoneNumberWarning || passwordWarning ? "!" : otherAccountsBadge, accountTabBarAvatar != nil, presentationData.reduceMotion)
}
self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] title, image, selectedImage, badgeValue, isAvatar in
self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] title, image, selectedImage, badgeValue, isAvatar, reduceMotion in
if let strongSelf = self {
strongSelf.tabBarItem.title = title
strongSelf.tabBarItem.image = image
strongSelf.tabBarItem.selectedImage = selectedImage
strongSelf.tabBarItem.animationName = isAvatar ? nil : "TabSettings"
strongSelf.tabBarItem.animationName = isAvatar || reduceMotion ? nil : "TabSettings"
strongSelf.tabBarItem.ringSelection = isAvatar
strongSelf.tabBarItem.badgeValue = badgeValue
}