Add manual account deletion

This commit is contained in:
Ilya Laktyushin 2022-06-28 22:22:21 +03:00
parent ecc9279281
commit 25a09c5451
27 changed files with 1383 additions and 81 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -7770,6 +7770,8 @@ Sorry for the inconvenience.";
"DeleteAccount.DeleteMyAccountTitle" = "Delete My Account"; "DeleteAccount.DeleteMyAccountTitle" = "Delete My Account";
"DeleteAccount.DeleteMyAccount" = "Delete My Account"; "DeleteAccount.DeleteMyAccount" = "Delete My Account";
"DeleteAccount.SavedMessages" = "Saved";
"DeleteAccount.ComeBackLater" = "Come Back Later"; "DeleteAccount.ComeBackLater" = "Come Back Later";
"DeleteAccount.Continue" = "Continue"; "DeleteAccount.Continue" = "Continue";
@ -7778,10 +7780,18 @@ Sorry for the inconvenience.";
"DeleteAccount.GroupsAndChannelsTitle" = "Your Groups and Channels"; "DeleteAccount.GroupsAndChannelsTitle" = "Your Groups and Channels";
"DeleteAccount.GroupsAndChannelsText" = "The groups and channels you created will either get new admins or become orphaned."; "DeleteAccount.GroupsAndChannelsText" = "The groups and channels you created will either get new admins or become orphaned.";
"DeleteAccount.GroupsAndChannelsInfo" = "You can transfer group and channel ownership to other users via Chat Info > Edit > Admins. [More info]()"; "DeleteAccount.GroupsAndChannelsInfo" = "You can transfer group and channel ownership to other users via Chat Info > Edit > Admins.";
"DeleteAccount.MessageHistoryTitle" = "Your Message History"; "DeleteAccount.MessageHistoryTitle" = "Your Message History";
"DeleteAccount.MessageHistoryText" = "Your chat partners will keep their message history with you, including the messages you shared in secret chats.\n\nYou can remove any messages for both sides at any time, but this will not be possible if you delete your account. [More info]()"; "DeleteAccount.MessageHistoryText" = "Your chat partners will keep their message history with you, including the messages you shared in secret chats.\n\nYou can remove any messages for both sides at any time, but this will not be possible if you delete your account.";
"DeleteAccount.DeleteMessagesURL" = "https://telegram.org/faq#q-can-i-delete-my-messages";
"DeleteAccount.EnterPhoneNumber" = "Enter Your Phone Number";
"DeleteAccount.InvalidPhoneNumberError" = "Invalid phone number. Please try again.";
"DeleteAccount.EnterPassword" = "Enter Your Password";
"DeleteAccount.InvalidPasswordError" = "Invalid password. Please try again.";
"DeleteAccount.ConfirmationAlertTitle" = "Proceed to Delete Your Account?"; "DeleteAccount.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.ConfirmationAlertText" = "Deleting your account will permanently delete your data!\n\nIt is imposible to reverse this action!";
@ -7801,3 +7811,8 @@ Sorry for the inconvenience.";
"Premium.Gift.Years_1" = "%@ Year"; "Premium.Gift.Years_1" = "%@ Year";
"Premium.Gift.Years_any" = "%@ Years"; "Premium.Gift.Years_any" = "%@ Years";
"Premium.GiftedTitle.3Month" = "[%@]() has gifted you a 3-month subscription for Telegram Premium";
"Premium.GiftedTitle.6Month" = "[%@]() has gifted you a 6-month subscription for Telegram Premium";
"Premium.GiftedTitle.12Month" = "[%@]() has gifted you a 12-month subscription for Telegram Premium";
"Premium.GiftedDescription" = "You now have access to additional features.";

View File

@ -120,7 +120,7 @@ class ChatListRecentPeersListItemNode: ListViewItemNode {
peersNode = currentPeersNode peersNode = currentPeersNode
peersNode.updateThemeAndStrings(theme: item.theme, strings: item.strings) peersNode.updateThemeAndStrings(theme: item.theme, strings: item.strings)
} else { } 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) self?.item?.peerSelected(peer)
}, peerContextAction: { peer, node, gesture in }, peerContextAction: { peer, node, gesture in
self?.item?.peerContextAction(peer, node, gesture) self?.item?.peerContextAction(peer, node, gesture)

View File

@ -1271,7 +1271,7 @@ open class NavigationController: UINavigationController, ContainableController,
let badgeNode = ASImageNode() let badgeNode = ASImageNode()
badgeNode.displaysAsynchronously = false badgeNode.displaysAsynchronously = false
badgeNode.image = UIImage(bundleImageName: "Components/BadgeTest") badgeNode.image = UIImage(bundleImageName: "Components/AppBadge")
self.badgeNode = badgeNode self.badgeNode = badgeNode
self.displayNode.addSubnode(badgeNode) self.displayNode.addSubnode(badgeNode)
} }

View File

@ -12,7 +12,7 @@ import ContextUI
import AccountContext import AccountContext
public enum HorizontalPeerItemMode { public enum HorizontalPeerItemMode {
case list case list(compact: Bool)
case actionSheet case actionSheet
} }
@ -25,13 +25,13 @@ public final class HorizontalPeerItem: ListViewItem {
let context: AccountContext let context: AccountContext
public let peer: EnginePeer public let peer: EnginePeer
let action: (EnginePeer) -> Void let action: (EnginePeer) -> Void
let contextAction: (EnginePeer, ASDisplayNode, ContextGesture?) -> Void let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?) -> Void)?
let isPeerSelected: (EnginePeer.Id) -> Bool let isPeerSelected: (EnginePeer.Id) -> Bool
let customWidth: CGFloat? let customWidth: CGFloat?
let presence: EnginePeer.Presence? let presence: EnginePeer.Presence?
let unreadBadge: (Int32, Bool)? 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.theme = theme
self.strings = strings self.strings = strings
self.mode = mode self.mode = mode
@ -111,11 +111,6 @@ public final class HorizontalPeerItemNode: ListViewItemNode {
item.action(item.peer) 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() { override public func didLoad() {
@ -186,10 +181,25 @@ public final class HorizontalPeerItemNode: ListViewItemNode {
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.peerNode.theme = itemTheme 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.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.frame = CGRect(origin: CGPoint(), size: itemLayout.size)
strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false) 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 let badgeBackgroundWidth: CGFloat
if let currentBadgeBackgroundImage = currentBadgeBackgroundImage { if let currentBadgeBackgroundImage = currentBadgeBackgroundImage {
strongSelf.badgeBackgroundNode.image = currentBadgeBackgroundImage strongSelf.badgeBackgroundNode.image = currentBadgeBackgroundImage

View File

@ -113,8 +113,9 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeTextLayout = TextNode.asyncLayout(self.textNode)
return { item, params, neighbors in return { item, params, neighbors in
let leftInset: CGFloat = 28.0 + params.leftInset let leftInset: CGFloat = 24.0 + params.leftInset
let topInset: CGFloat = 124.0 let iconSize = CGSize(width: 140.0, height: 140.0)
let topInset: CGFloat = iconSize.height - 4.0
let spacing: CGFloat = 5.0 let spacing: CGFloat = 5.0
let attributedTitle = NSAttributedString(string: item.title ?? "", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) let attributedTitle = NSAttributedString(string: item.title ?? "", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
@ -123,9 +124,9 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
return (TelegramTextAttributes.URL, contents) return (TelegramTextAttributes.URL, contents)
})) }))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var contentSize = CGSize(width: params.width, height: topInset + textLayout.size.height) var contentSize = CGSize(width: params.width, height: topInset + textLayout.size.height)
if let _ = item.title { if let _ = item.title {
@ -138,13 +139,12 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
return (layout, { [weak self] in return (layout, { [weak self] in
if let strongSelf = self { if let strongSelf = self {
if strongSelf.item == nil { 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.animationNode.visibility = true
} }
strongSelf.item = item strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string 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.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize) strongSelf.animationNode.updateLayout(size: iconSize)

View File

@ -275,9 +275,8 @@ class GiftAvatarComponent: Component {
} }
self.hasIdleAnimations = component.hasIdleAnimations self.hasIdleAnimations = component.hasIdleAnimations
let avatarSize = CGSize(width: 100.0, height: 100.0) let avatarSize = CGSize(width: 100.0, height: 100.0)
self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: component.peer, size: avatarSize, font: avatarPlaceholderFont(size: 78.0), fullSize: true)) self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: component.peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true))
self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 63.0), size: avatarSize) self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 63.0), size: avatarSize)
return availableSize return availableSize

View File

@ -84,6 +84,8 @@ public final class SelectablePeerNode: ASDisplayNode {
private var peer: EngineRenderedPeer? 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) { public var theme: SelectablePeerNodeTheme = SelectablePeerNodeTheme(textColor: .black, secretTextColor: .green, selectedTextColor: .blue, checkBackgroundColor: .white, checkFillColor: .blue, checkColor: .white, avatarPlaceholderColor: .white) {
didSet { didSet {
if !self.theme.isEqual(to: oldValue) { if !self.theme.isEqual(to: oldValue) {
@ -147,7 +149,7 @@ public final class SelectablePeerNode: ASDisplayNode {
let text: String let text: String
var overrideImage: AvatarNodeImageOverride? var overrideImage: AvatarNodeImageOverride?
if peer.peerId == context.account.peerId { if peer.peerId == context.account.peerId {
text = strings.DialogList_SavedMessages text = self.compact ? strings.DeleteAccount_SavedMessages : strings.DialogList_SavedMessages
overrideImage = .savedMessagesIcon overrideImage = .savedMessagesIcon
} else if peer.peerId.isReplies { } else if peer.peerId.isReplies {
text = strings.DialogList_Replies text = strings.DialogList_Replies

View File

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

View File

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

View File

@ -13,25 +13,47 @@ import AlertUI
import PresentationDataUtils import PresentationDataUtils
import UrlHandling import UrlHandling
import InviteLinksUI import InviteLinksUI
import CountrySelectionUI
import PhoneInputNode
private struct DeleteAccountDataArguments { private struct DeleteAccountDataArguments {
let context: AccountContext let context: AccountContext
let openLink: (String) -> Void let openLink: (String) -> Void
let selectCountryCode: () -> Void
let updatePassword: (String) -> Void
let proceed: () -> Void
} }
private enum DeleteAccountDataSection: Int32 { private enum DeleteAccountDataSection: Int32 {
case header
case main 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 { private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable {
case header(PresentationTheme, String, String, String) case header(PresentationTheme, String, String, String)
case peers(PresentationTheme, [EnginePeer])
case peers(PresentationTheme, [Peer]) case phone(PresentationTheme, PresentationStrings)
case password(PresentationTheme, String)
case info(PresentationTheme, String) case info(PresentationTheme, String)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .header, .peers, .info: case .header:
return DeleteAccountDataSection.header.rawValue
case .peers, .info, .phone, .password:
return DeleteAccountDataSection.main.rawValue return DeleteAccountDataSection.main.rawValue
} }
} }
@ -43,7 +65,11 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable {
case .peers: case .peers:
return 1 return 1
case .info: case .info:
return 2
case .phone:
return 3 return 3
case .password:
return 4
} }
} }
@ -56,7 +82,7 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable {
return false return false
} }
case let .peers(lhsTheme, lhsPeers): case let .peers(lhsTheme, lhsPeers):
if case let .peers(rhsTheme, rhsPeers) = rhs, lhsTheme === rhsTheme, arePeerArraysEqual(lhsPeers, rhsPeers) { if case let .peers(rhsTheme, rhsPeers) = rhs, lhsTheme === rhsTheme, lhsPeers == rhsPeers {
return true return true
} else { } else {
return false return false
@ -67,6 +93,19 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable {
} else { } else {
return false return false
} }
case let .phone(lhsTheme, lhsStrings):
if case let .phone(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
return true
} else {
return false
}
case let .password(lhsTheme, lhsPlaceholder):
if case let .password(rhsTheme, rhsPlaceholder) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder {
return true
} else {
return false
}
} }
} }
@ -80,14 +119,26 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable {
case let .header(theme, animation, title, text): 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) return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: text, animationName: animation, sectionId: self.section, linkAction: nil)
case let .peers(_, peers): case let .peers(_, peers):
return ItemListTextItem(presentationData: presentationData, text: .plain(peers.first?.debugDisplayTitle ?? ""), sectionId: self.section) return DeleteAccountPeersItem(context: arguments.context, theme: presentationData.theme, strings: presentationData.strings, peers: peers, sectionId: self.section)
case let .info(_, text): case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case .phone:
return DeleteAccountPhoneItem(theme: presentationData.theme, strings: presentationData.strings, value: (nil, nil, ""), sectionId: self.section, selectCountryCode: {
arguments.selectCountryCode()
}, updated: { _ in
})
case let .password(_, placeholder):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: "", placeholder: placeholder, type: .password, returnKeyType: .done, tag: DeleteAccountEntryTag.password, sectionId: self.section, textUpdated: { value in
arguments.updatePassword(value)
}, action: {
arguments.proceed()
})
} }
} }
} }
private func deleteAccountDataEntries(presentationData: PresentationData, mode: DeleteAccountDataMode, peers: [Peer]) -> [DeleteAccountDataEntry] { private func deleteAccountDataEntries(presentationData: PresentationData, mode: DeleteAccountDataMode, peers: [EnginePeer]) -> [DeleteAccountDataEntry] {
var entries: [DeleteAccountDataEntry] = [] var entries: [DeleteAccountDataEntry] = []
let headerTitle: String let headerTitle: String
@ -96,24 +147,45 @@ private func deleteAccountDataEntries(presentationData: PresentationData, mode:
switch mode { switch mode {
case .peers: case .peers:
headerAnimation = "" headerAnimation = "Delete1"
headerTitle = presentationData.strings.DeleteAccount_CloudStorageTitle headerTitle = presentationData.strings.DeleteAccount_CloudStorageTitle
headerText = presentationData.strings.DeleteAccount_CloudStorageText headerText = presentationData.strings.DeleteAccount_CloudStorageText
case .groups: case .groups:
headerAnimation = "" headerAnimation = "Delete2"
headerTitle = presentationData.strings.DeleteAccount_GroupsAndChannelsTitle headerTitle = presentationData.strings.DeleteAccount_GroupsAndChannelsTitle
headerText = presentationData.strings.DeleteAccount_GroupsAndChannelsText headerText = presentationData.strings.DeleteAccount_GroupsAndChannelsText
case .messages: case .messages:
headerAnimation = "" headerAnimation = "Delete3"
headerTitle = presentationData.strings.DeleteAccount_MessageHistoryTitle headerTitle = presentationData.strings.DeleteAccount_MessageHistoryTitle
headerText = presentationData.strings.DeleteAccount_MessageHistoryText headerText = presentationData.strings.DeleteAccount_MessageHistoryText
case .phone:
headerAnimation = "Delete4"
headerTitle = presentationData.strings.DeleteAccount_EnterPhoneNumber
headerText = ""
case .password:
headerAnimation = "Delete5"
headerTitle = presentationData.strings.DeleteAccount_EnterPassword
headerText = ""
} }
entries.append(.header(presentationData.theme, headerAnimation, headerTitle, headerText)) entries.append(.header(presentationData.theme, headerAnimation, headerTitle, headerText))
entries.append(.peers(presentationData.theme, peers))
if case .groups = mode { switch mode {
entries.append(.info(presentationData.theme, presentationData.strings.DeleteAccount_GroupsAndChannelsInfo)) 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 return entries
@ -121,55 +193,150 @@ private func deleteAccountDataEntries(presentationData: PresentationData, mode:
enum DeleteAccountDataMode { enum DeleteAccountDataMode {
case peers case peers
case groups case groups([EnginePeer])
case messages case messages
case phone
case password
} }
func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDataMode) -> ViewController { private struct DeleteAccountDataState: Equatable {
var password: String
var isLoading: Bool
static func == (lhs: DeleteAccountDataState, rhs: DeleteAccountDataState) -> Bool {
return lhs.password == rhs.password && lhs.isLoading == rhs.isLoading
}
}
func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDataMode, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> ViewController {
let initialState = DeleteAccountDataState(password: "", isLoading: false)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((DeleteAccountDataState) -> DeleteAccountDataState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var replaceTopControllerImpl: ((ViewController) -> Void)? var replaceTopControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> 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 let arguments = DeleteAccountDataArguments(context: context, openLink: { _ in
}, selectCountryCode: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = AuthorizationSequenceCountrySelectionController(strings: presentationData.strings, theme: presentationData.theme)
controller.completeWithCountryCode = { code, name in
updateCountryCodeImpl?(Int32(code), name)
activateInputImpl?()
}
dismissInputImpl?()
pushControllerImpl?(controller)
}, updatePassword: { password in
updateState { current in
var updated = current
updated.password = password
return updated
}
}, proceed: {
proceedImpl?()
}) })
let peers: Signal<[Peer], NoError> = .single([]) let preloadedGroupPeers = Promise<[EnginePeer]>([])
let peers: Signal<[EnginePeer], NoError>
switch mode {
case .peers:
peers = combineLatest(
context.engine.peers.recentPeers()
|> map { recentPeers -> [EnginePeer] in
if case let .peers(peers) = recentPeers {
return peers.map { EnginePeer($0) }
} else {
return []
}
},
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
) |> map { recentPeers, accountPeer -> [EnginePeer] in
var peers: [EnginePeer] = []
if let accountPeer = accountPeer {
peers.append(accountPeer)
}
peers.append(contentsOf: recentPeers.prefix(9))
return peers
}
preloadedGroupPeers.set(context.engine.peers.adminedPublicChannels(scope: .all)
|> map { peers -> [EnginePeer] in
return peers.map { EnginePeer($0) }
})
case let .groups(preloadedPeers):
peers = .single(preloadedPeers.shuffled())
default:
peers = .single([])
}
let signal = combineLatest(queue: .mainQueue(), let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData, context.sharedContext.presentationData,
peers peers,
statePromise.get()
) )
|> map { presentationData, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in |> map { presentationData, peers, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?() dismissImpl?()
}) })
let footerItem = DeleteAccountFooterItem(theme: presentationData.theme, title: presentationData.strings.DeleteAccount_ComeBackLater, secondaryTitle: presentationData.strings.DeleteAccount_Continue, action: { var focusItemTag: DeleteAccountEntryTag?
var buttonTitle: String
switch mode {
case .phone:
buttonTitle = ""
case .password:
buttonTitle = ""
focusItemTag = .password
default:
buttonTitle = presentationData.strings.DeleteAccount_ComeBackLater
}
let rightNavigationButton: ItemListNavigationButton?
if state.isLoading {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
rightNavigationButton = nil
}
let footerItem = DeleteAccountFooterItem(theme: presentationData.theme, title: buttonTitle, secondaryTitle: presentationData.strings.DeleteAccount_Continue, action: {
dismissImpl?() dismissImpl?()
}, secondaryAction: { }, secondaryAction: {
let nextMode: DeleteAccountDataMode? proceedImpl?()
switch mode {
case .peers:
nextMode = .groups
case .groups:
nextMode = .messages
case .messages:
nextMode = nil
}
if let nextMode = nextMode {
let controller = deleteAccountDataController(context: context, mode: nextMode)
replaceTopControllerImpl?(controller)
}
}) })
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DeleteAccount_DeleteMyAccountTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) 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, footerItem: footerItem) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deleteAccountDataEntries(presentationData: presentationData, mode: mode, peers: peers), style: .blocks, focusItemTag: focusItemTag, footerItem: footerItem)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
let controller = ItemListController(context: context, state: signal, tabBarItem: nil) 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 replaceTopControllerImpl = { [weak controller] c in
if let navigationController = controller?.navigationController as? NavigationController { if let navigationController = controller?.navigationController as? NavigationController {
navigationController.pushViewController(c, completion: { [weak navigationController, weak controller, weak c] in navigationController.pushViewController(c, completion: { [weak navigationController, weak controller, weak c] in
@ -184,7 +351,172 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat
dismissImpl = { [weak controller] in dismissImpl = { [weak controller] in
let _ = controller?.dismiss() let _ = controller?.dismiss()
} }
updateCodeImpl = { [weak controller] in
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? DeleteAccountPhoneItemNode {
itemNode.updateCountryCode()
}
}
}
activateInputImpl = { [weak controller] in
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? DeleteAccountPhoneItemNode {
itemNode.activateInput()
}
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
controller.didAppear = { firstTime in
if !firstTime {
return
}
activateInputImpl?()
}
updateCountryCodeImpl = { [weak controller] code, name in
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? DeleteAccountPhoneItemNode {
itemNode.updateCountryCode(code: code, name: name)
}
}
}
proceedImpl = { [weak controller] in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let action: ([EnginePeer]) -> Void = { preloadedPeers in
let nextMode: DeleteAccountDataMode?
switch mode {
case .peers:
if !preloadedPeers.isEmpty {
nextMode = .groups(preloadedPeers)
} else {
nextMode = .messages
}
case .groups:
nextMode = .messages
case .messages:
nextMode = .phone
case .phone:
if let twoStepAuthData = twoStepAuthData, case .set = twoStepAuthData {
nextMode = .password
} else {
nextMode = nil
}
case .password:
nextMode = nil
}
if let nextMode = nextMode {
let controller = deleteAccountDataController(context: context, mode: nextMode, twoStepAuthData: twoStepAuthData)
replaceTopControllerImpl?(controller)
} else {
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.DeleteAccount_ConfirmationAlertTitle, text: presentationData.strings.DeleteAccount_ConfirmationAlertText, actions: [TextAlertAction(type: .destructiveAction, title: presentationData.strings.DeleteAccount_ConfirmationAlertDelete, action: {
updateState { current in
var updated = current
updated.isLoading = true
return updated
}
let accountId = context.account.id
let accountManager = context.sharedContext.accountManager
let _ = (context.engine.auth.deleteAccount(reason: "Manual")
|> deliverOnMainQueue).start(error: { _ in
updateState { current in
var updated = current
updated.isLoading = false
return updated
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
}, completed: {
dismissImpl?()
let _ = logoutFromAccount(id: accountId, accountManager: accountManager, alreadyLoggedOutRemotely: true).start()
})
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]))
}
}
switch mode {
case .peers:
let _ = (preloadedGroupPeers.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peers in
action(peers)
})
case .phone:
var phoneNumber: String?
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? DeleteAccountPhoneItemNode {
phoneNumber = itemNode.phoneNumber
}
}
if let phoneNumber = phoneNumber, phoneNumber.count > 4 {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue)
.start(next: { accountPeer in
if let accountPeer = accountPeer, case let .user(user) = accountPeer, var phone = user.phone {
if !phone.hasPrefix("+") {
phone = "+\(phone)"
}
if phone != phoneNumber {
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.DeleteAccount_InvalidPhoneNumberError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
return
}
action([])
}
})
}
case .password:
let state = stateValue.with { $0 }
if !state.password.isEmpty {
updateState { current in
var updated = current
updated.isLoading = true
return updated
}
let _ = (context.engine.auth.requestTwoStepVerifiationSettings(password: state.password)
|> deliverOnMainQueue).start(error: { error in
updateState { current in
var updated = current
updated.isLoading = false
return updated
}
let text: String
switch error {
case .limitExceeded:
text = presentationData.strings.LoginPassword_FloodError
case .invalidPassword:
text = presentationData.strings.DeleteAccount_InvalidPasswordError
default:
text = presentationData.strings.Login_UnknownError
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
}, completed: {
updateState { current in
var updated = current
updated.isLoading = false
return updated
}
action([])
})
return
}
default:
action([])
}
}
return controller return controller
} }

View File

@ -44,8 +44,9 @@ final class DeleteAccountFooterItem: ItemListControllerFooterItem {
final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode { final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode {
private let backgroundNode: NavigationBackgroundNode private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private let clipNode: ASDisplayNode
private let buttonNode: SolidRoundedButtonNode private let buttonNode: SolidRoundedButtonNode
private let secondaryButtonNode: HighlightTrackingButtonNode private let secondaryButtonNode: HighlightableButtonNode
private var validLayout: ContainerViewLayout? private var validLayout: ContainerViewLayout?
@ -64,16 +65,20 @@ final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode {
self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor) self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor)
self.separatorNode = ASDisplayNode() 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.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true)
self.secondaryButtonNode = HighlightTrackingButtonNode() self.secondaryButtonNode = HighlightableButtonNode()
super.init() super.init()
self.addSubnode(self.backgroundNode) self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.clipNode)
self.addSubnode(self.secondaryButtonNode) self.clipNode.addSubnode(self.buttonNode)
self.clipNode.addSubnode(self.secondaryButtonNode)
self.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside) self.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside)
@ -121,8 +126,19 @@ final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode {
let secondaryButtonSize = self.secondaryButtonNode.measure(CGSize(width: buttonWidth, height: CGFloat.greatestFiniteMagnitude)) let secondaryButtonSize = self.secondaryButtonNode.measure(CGSize(width: buttonWidth, height: CGFloat.greatestFiniteMagnitude))
var panelHeight: CGFloat = buttonHeight + topInset + spacing + secondaryButtonSize.height + bottomInset var panelHeight: CGFloat = buttonHeight + topInset + spacing + secondaryButtonSize.height + bottomInset
var buttonOffset: CGFloat = 0.0
let totalPanelHeight: CGFloat 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 { if let inputHeight = layout.inputHeight, inputHeight > 0.0 {
panelHeight += buttonOffset
totalPanelHeight = panelHeight + insets.bottom totalPanelHeight = panelHeight + insets.bottom
} else { } else {
panelHeight += insets.bottom panelHeight += insets.bottom
@ -130,13 +146,15 @@ final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode {
} }
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + topInset), size: CGSize(width: buttonWidth, height: buttonHeight)))
transition.updateFrame(node: self.secondaryButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - secondaryButtonSize.width) / 2.0), y: panelFrame.minY + topInset + buttonHeight + spacing), size: secondaryButtonSize))
transition.updateFrame(node: self.backgroundNode, frame: panelFrame) transition.updateFrame(node: self.backgroundNode, frame: panelFrame)
self.backgroundNode.update(size: panelFrame.size, transition: transition) 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))) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel)))
return panelHeight return panelHeight

View File

@ -15,14 +15,17 @@ import PresentationDataUtils
import UrlHandling import UrlHandling
import AccountUtils import AccountUtils
import PremiumUI import PremiumUI
import PasswordSetupUI
private struct DeleteAccountOptionsArguments { private struct DeleteAccountOptionsArguments {
let changePhoneNumber: () -> Void let changePhoneNumber: () -> Void
let addAccount: () -> Void let addAccount: () -> Void
let setupPrivacy: () -> Void let setupPrivacy: () -> Void
let setTwoStepAuth: () -> Void let setupTwoStepAuth: () -> Void
let setPasscode: () -> Void let setPasscode: () -> Void
let clearCache: () -> Void let clearCache: () -> Void
let clearSyncedContacts: () -> Void
let deleteChats: () -> Void
let contactSupport: () -> Void let contactSupport: () -> Void
let deleteAccount: () -> Void let deleteAccount: () -> Void
} }
@ -111,8 +114,8 @@ private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable {
arguments.setupPrivacy() arguments.setupPrivacy()
}) })
case let .setTwoStepAuth(_, title, text): case let .setTwoStepAuth(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.setPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetTwoStepAuth, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.setTwoStepAuth() arguments.setupTwoStepAuth()
}) })
case let .setPasscode(_, title, text): 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: { return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
@ -124,11 +127,11 @@ private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable {
}) })
case let .clearSyncedContacts(_, title, text): 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: { return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.clearSynced, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.clearCache() arguments.clearSyncedContacts()
}) })
case let .deleteChats(_, title, text): 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: { return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteChats, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.clearCache() arguments.deleteChats()
}) })
case let .contactSupport(_, title, text): 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: { return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
@ -168,14 +171,14 @@ private func deleteAccountOptionsEntries(presentationData: PresentationData, can
return entries return entries
} }
public func deleteAccountOptionsController(context: AccountContext, navigationController: NavigationController, hasTwoStepAuth: Bool) -> ViewController { public func deleteAccountOptionsController(context: AccountContext, navigationController: NavigationController, hasTwoStepAuth: Bool, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)?
var replaceTopControllerImpl: ((ViewController, Bool) -> Void)? var replaceTopControllerImpl: ((ViewController, Bool) -> Void)?
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
let supportPeerDisposable = MetaDisposable() let supportPeerDisposable = MetaDisposable()
let arguments = DeleteAccountOptionsArguments(changePhoneNumber: { let arguments = DeleteAccountOptionsArguments(changePhoneNumber: {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.engine.account.peerId)) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.engine.account.peerId))
|> deliverOnMainQueue).start(next: { accountPeer in |> deliverOnMainQueue).start(next: { accountPeer in
@ -224,9 +227,30 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo
} }
}) })
}, setupPrivacy: { }, setupPrivacy: {
replaceTopControllerImpl?(makePrivacyAndSecurityController(context: context), false)
}, setupTwoStepAuth: {
if let data = twoStepAuthData {
switch data {
case .set:
break
case let .notSet(pendingEmail):
if pendingEmail == nil {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .intro(.init(
title: presentationData.strings.TwoFactorSetup_Intro_Title,
text: presentationData.strings.TwoFactorSetup_Intro_Text,
actionText: presentationData.strings.TwoFactorSetup_Intro_Action,
doneText: presentationData.strings.TwoFactorSetup_Done_Action
)))
}, setTwoStepAuth: { replaceTopControllerImpl?(controller, false)
return
}
}
}
let controller = twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: twoStepAuthData.flatMap({ Signal<TwoStepVerificationUnlockSettingsControllerData, NoError>.single(.access(configuration: $0)) })))
replaceTopControllerImpl?(controller, false)
}, setPasscode: { }, setPasscode: {
let _ = passcodeOptionsAccessController(context: context, pushController: { controller in let _ = passcodeOptionsAccessController(context: context, pushController: { controller in
replaceTopControllerImpl?(controller, false) replaceTopControllerImpl?(controller, false)
@ -241,11 +265,44 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo
}, clearCache: { }, clearCache: {
pushControllerImpl?(storageUsageController(context: context)) pushControllerImpl?(storageUsageController(context: context))
dismissImpl?() dismissImpl?()
}, clearSyncedContacts: {
replaceTopControllerImpl?(dataPrivacyController(context: context), false)
}, deleteChats: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var faqUrl = presentationData.strings.DeleteAccount_DeleteMessagesURL
if faqUrl == "DeleteAccount.DeleteMessagesURL" || faqUrl.isEmpty {
faqUrl = "https://telegram.org/faq#q-can-i-delete-my-messages"
}
let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl)
let resolvedUrlPromise = Promise<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 }, contactSupport: { [weak navigationController] in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let supportPeer = Promise<PeerId?>() let supportPeer = Promise<PeerId?>()
supportPeer.set(context.engine.peers.supportPeerId()) supportPeer.set(context.engine.peers.supportPeerId())
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var faqUrl = presentationData.strings.Settings_FAQ_URL var faqUrl = presentationData.strings.Settings_FAQ_URL
if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty {
faqUrl = "https://telegram.org/faq#general" faqUrl = "https://telegram.org/faq#general"
@ -289,7 +346,7 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo
}) })
]), nil) ]), nil)
}, deleteAccount: { }, deleteAccount: {
let controller = deleteAccountDataController(context: context, mode: .peers) let controller = deleteAccountDataController(context: context, mode: .peers, twoStepAuthData: twoStepAuthData)
replaceTopControllerImpl?(controller, true) replaceTopControllerImpl?(controller, true)
}) })
@ -337,7 +394,18 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo
} }
}) })
} else { } else {
navigationController?.replaceTopController(c, animated: true) if c is PrivacyAndSecurityControllerImpl {
if let navigationController = navigationController {
if let existing = navigationController.viewControllers.first(where: { $0 is PrivacyAndSecurityControllerImpl }) as? ViewController {
existing.scrollToTop?()
dismissImpl?()
} else {
navigationController.replaceTopController(c, animated: true)
}
}
} else {
navigationController?.replaceTopController(c, animated: true)
}
} }
} }
dismissImpl = { [weak controller] in dismissImpl = { [weak controller] in

View File

@ -0,0 +1,296 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import HorizontalPeerItem
import AccountContext
import MergeLists
private struct PeersEntry: Comparable, Identifiable {
let index: Int
let peer: EnginePeer
let theme: PresentationTheme
let strings: PresentationStrings
var stableId: EnginePeer.Id {
return self.peer.id
}
static func ==(lhs: PeersEntry, rhs: PeersEntry) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
return true
}
static func <(lhs: PeersEntry, rhs: PeersEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext) -> ListViewItem {
return HorizontalPeerItem(theme: self.theme, strings: self.strings, mode: .list(compact: true), context: context, peer: self.peer, presence: nil, unreadBadge: nil, action: { _ in }, contextAction: nil, isPeerSelected: { _ in return false }, customWidth: nil)
}
}
private struct DeleteAccountPeersItemNodeTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let firstTime: Bool
let animated: Bool
}
private func preparedPeersTransition(context: AccountContext, from fromEntries: [PeersEntry], to toEntries: [PeersEntry], firstTime: Bool, animated: Bool) -> DeleteAccountPeersItemNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context), directionHint: .Down) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context), directionHint: nil) }
return DeleteAccountPeersItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, animated: animated)
}
class DeleteAccountPeersItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let peers: [EnginePeer]
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peers: [EnginePeer], sectionId: ItemListSectionId) {
self.context = context
self.theme = theme
self.strings = strings
self.peers = peers
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<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) {
let currentItem = self.item
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
print(themeUpdated)
let contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 109.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.dataPromise.set(.single((item.context, item.peers, item.theme, item.strings)))
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let listInsets = UIEdgeInsets(top: params.leftInset, left: 0.0, bottom: params.rightInset, right: 0.0)
strongSelf.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: params.width)
strongSelf.listView.position = CGPoint(x: params.width / 2.0, y: contentSize.height / 2.0)
strongSelf.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: params.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
strongSelf.dequeueTransitions()
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

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 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 { 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 statePromise = ValuePromise(PrivacyAndSecurityControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: PrivacyAndSecurityControllerState()) let stateValue = Atomic(value: PrivacyAndSecurityControllerState())
@ -492,6 +496,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
var pushControllerImpl: ((ViewController, Bool) -> Void)? var pushControllerImpl: ((ViewController, Bool) -> Void)?
var replaceTopControllerImpl: ((ViewController) -> Void)? var replaceTopControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)?
var getNavigationControllerImpl: (() -> NavigationController?)?
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
@ -822,12 +827,26 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
6 * 30 * 24 * 60 * 60, 6 * 30 * 24 * 60 * 60,
365 * 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: { return ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: value), action: {
dismissAction() dismissAction()
timeoutAction(value) 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([ controller.setItemGroups([
ActionSheetItemGroup(items: timeoutItems), ActionSheetItemGroup(items: timeoutItems),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
@ -886,7 +905,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
actionsDisposable.dispose() actionsDisposable.dispose()
} }
let controller = ItemListController(context: context, state: signal) let controller = PrivacyAndSecurityControllerImpl(context: context, state: signal)
pushControllerImpl = { [weak controller] c, animated in pushControllerImpl = { [weak controller] c, animated in
(controller?.navigationController as? NavigationController)?.pushViewController(c, animated: animated) (controller?.navigationController as? NavigationController)?.pushViewController(c, animated: animated)
} }
@ -896,7 +915,10 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
presentControllerImpl = { [weak controller] c in presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} }
getNavigationControllerImpl = { [weak controller] in
return (controller?.navigationController as? NavigationController)
}
controller.didAppear = { _ in controller.didAppear = { _ in
updateHasTwoStepAuth() updateHasTwoStepAuth()
} }

View File

@ -206,7 +206,7 @@ private func twoStepVerificationUnlockSettingsControllerEntries(presentationData
if let pendingEmail = pendingEmail { if let pendingEmail = pendingEmail {
entries.append(.pendingEmailConfirmInfo(presentationData.theme, presentationData.strings.TwoStepAuth_SetupPendingEmail(pendingEmail.email.pattern).string)) 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(.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 + "]()"))*/ /*entries.append(.pendingEmailInfo(presentationData.theme, presentationData.strings.TwoStepAuth_ConfirmationText + "\n\n\(pendingEmailAndValue.pendingEmail.pattern)\n\n[" + presentationData.strings.TwoStepAuth_ConfirmationAbort + "]()"))*/
} else { } else {

View File

@ -91,6 +91,7 @@ public struct PresentationResourcesSettings {
public static let changePhoneNumber = renderIcon(name: "Settings/Menu/ChangePhoneNumber") public static let changePhoneNumber = renderIcon(name: "Settings/Menu/ChangePhoneNumber")
public static let deleteAddAccount = renderIcon(name: "Settings/Menu/DeleteAddAccount") 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 deleteSetPasscode = renderIcon(name: "Settings/Menu/FaceId")
public static let deleteChats = renderIcon(name: "Settings/Menu/DeleteChats") public static let deleteChats = renderIcon(name: "Settings/Menu/DeleteChats")
public static let clearSynced = renderIcon(name: "Settings/Menu/ClearSynced") public static let clearSynced = renderIcon(name: "Settings/Menu/ClearSynced")

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