diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 706d2a2559..22672c2f83 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -791,6 +791,7 @@ public protocol SharedAccountContext: AnyObject { func makeCreateGroupController(context: AccountContext, peerIds: [PeerId], initialTitle: String?, mode: CreateGroupMode, completion: ((PeerId, @escaping () -> Void) -> Void)?) -> ViewController func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?) -> ViewController func makePrivacyAndSecurityController(context: AccountContext) -> ViewController + func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController func makeStorageManagementController(context: AccountContext) -> ViewController func navigateToChatController(_ params: NavigateToChatControllerParams) func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index ac4db22ac0..3097a0f25d 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -185,7 +185,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}) interaction.isInlineMode = isInlineMode let items = (0 ..< 2).map { _ -> ChatListItem in diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 239cae747b..db6e7b934d 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2005,6 +2005,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.listNode.clearHighlightAnimated(true) }) }, openStorageManagement: { + }, openPasswordSetup: { }) chatListInteraction.isSearchMode = true @@ -3206,7 +3207,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}) var isInlineMode = false if case .topics = key { isInlineMode = false diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index d42a0046aa..10271ba496 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -92,6 +92,7 @@ public final class ChatListNodeInteraction { let present: (ViewController) -> Void let openForumThread: (EnginePeer.Id, Int64) -> Void let openStorageManagement: () -> Void + let openPasswordSetup: () -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -134,7 +135,8 @@ public final class ChatListNodeInteraction { activateChatPreview: @escaping (ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, present: @escaping (ViewController) -> Void, openForumThread: @escaping (EnginePeer.Id, Int64) -> Void, - openStorageManagement: @escaping () -> Void + openStorageManagement: @escaping () -> Void, + openPasswordSetup: @escaping () -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -165,6 +167,7 @@ public final class ChatListNodeInteraction { self.animationRenderer = animationRenderer self.openForumThread = openForumThread self.openStorageManagement = openStorageManagement + self.openPasswordSetup = openPasswordSetup } } @@ -570,9 +573,14 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL ), directionHint: entry.directionHint) case let .ArchiveIntro(presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) - case let .StorageInfo(presentationData, sizeFraction): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, sizeFraction: sizeFraction, action: { [weak nodeInteraction] in - nodeInteraction?.openStorageManagement() + case let .Notice(presentationData, notice): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] in + switch notice { + case .clearStorage: + nodeInteraction?.openStorageManagement() + case .setupPassword: + nodeInteraction?.openPasswordSetup() + } }), directionHint: entry.directionHint) } } @@ -785,9 +793,14 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL ), directionHint: entry.directionHint) case let .ArchiveIntro(presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) - case let .StorageInfo(presentationData, sizeFraction): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, sizeFraction: sizeFraction, action: { [weak nodeInteraction] in - nodeInteraction?.openStorageManagement() + case let .Notice(presentationData, notice): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] in + switch notice { + case .clearStorage: + nodeInteraction?.openStorageManagement() + case .setupPassword: + nodeInteraction?.openPasswordSetup() + } }), directionHint: entry.directionHint) case .HeaderEntry: return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) @@ -1277,6 +1290,20 @@ public final class ChatListNode: ListView { } let controller = self.context.sharedContext.makeStorageManagementController(context: self.context) self.push?(controller) + }, openPasswordSetup: { [weak self] in + guard let self else { + return + } + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in + guard let self else { + return + } + let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).start() + }) + + let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context) + self.push?(controller) }) nodeInteraction.isInlineMode = isInlineMode @@ -1342,6 +1369,32 @@ public final class ChatListNode: ListView { displayArchiveIntro = .single(false) } + let suggestPasswordSetup: Signal + if case .chatList(groupId: .root) = location, chatListFilter == nil { + suggestPasswordSetup = combineLatest( + getServerProvidedSuggestions(account: context.account), + context.engine.auth.twoStepVerificationConfiguration() + ) + |> map { suggestions, configuration -> Bool in + var notSet = false + switch configuration { + case let .notSet(pendingEmail): + if pendingEmail == nil { + notSet = true + } + case .set: + break + } + if !notSet { + return false + } + return suggestions.contains(.setupPassword) + } + |> distinctUntilChanged + } else { + suggestPasswordSetup = .single(false) + } + let storageInfo: Signal if !"".isEmpty, case .chatList(groupId: .root) = location, chatListFilter == nil { let totalSizeSignal = combineLatest(context.account.postbox.mediaBox.storageBox.totalSize(), context.account.postbox.mediaBox.cacheStorageBox.totalSize()) @@ -1431,13 +1484,13 @@ public final class ChatListNode: ListView { let currentPeerId: EnginePeer.Id = context.account.peerId - let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, savedMessagesPeer, chatListViewUpdate, self.statePromise.get()) - |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, savedMessagesPeer, updateAndFilter, state) -> Signal in + let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestPasswordSetup, savedMessagesPeer, chatListViewUpdate, self.statePromise.get()) + |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestPasswordSetup, savedMessagesPeer, updateAndFilter, state) -> Signal in let (update, filter) = updateAndFilter let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) - let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, storageInfo: storageInfo, mode: mode, chatListLocation: location) + let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, storageInfo: storageInfo, suggestPasswordSetup: suggestPasswordSetup, mode: mode, chatListLocation: location) let entries = rawEntries.filter { entry in switch entry { case let .PeerEntry(peerEntry): @@ -2467,7 +2520,7 @@ public final class ChatListNode: ListView { } else { break loop } - case .ArchiveIntro, .StorageInfo, .HeaderEntry, .AdditionalCategory: + case .ArchiveIntro, .Notice, .HeaderEntry, .AdditionalCategory: break } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 4c9a2e2a5f..271181d2b5 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -13,7 +13,7 @@ enum ChatListNodeEntryId: Hashable { case ThreadId(Int64) case GroupId(EngineChatList.Group) case ArchiveIntro - case StorageInfo + case Notice case additionalCategory(Int) } @@ -46,6 +46,11 @@ public enum ChatListNodeEntryPromoInfo: Equatable { case psa(type: String, message: String?) } +enum ChatListNotice: Equatable { + case clearStorage(sizeFraction: Double) + case setupPassword +} + enum ChatListNodeEntry: Comparable, Identifiable { struct PeerEntryData: Equatable { var index: EngineChatList.Item.Index @@ -235,7 +240,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { case HoleEntry(EngineMessage.Index, theme: PresentationTheme) case GroupReferenceEntry(index: EngineChatList.Item.Index, presentationData: ChatListPresentationData, groupId: EngineChatList.Group, peers: [EngineChatList.GroupItem.Item], message: EngineMessage?, editing: Bool, unreadCount: Int, revealed: Bool, hiddenByDefault: Bool) case ArchiveIntro(presentationData: ChatListPresentationData) - case StorageInfo(presentationData: ChatListPresentationData, sizeFraction: Double) + case Notice(presentationData: ChatListPresentationData, notice: ChatListNotice) case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData) var sortIndex: ChatListNodeEntrySortIndex { @@ -250,7 +255,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { return .index(index) case .ArchiveIntro: return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor)) - case .StorageInfo: + case .Notice: return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor.successor)) case let .AdditionalCategory(index, _, _, _, _, _, _): return .additionalCategory(index) @@ -274,8 +279,8 @@ enum ChatListNodeEntry: Comparable, Identifiable { return .GroupId(groupId) case .ArchiveIntro: return .ArchiveIntro - case .StorageInfo: - return .StorageInfo + case .Notice: + return .Notice case let .AdditionalCategory(_, id, _, _, _, _, _): return .additionalCategory(id) } @@ -348,8 +353,8 @@ enum ChatListNodeEntry: Comparable, Identifiable { } else { return false } - case let .StorageInfo(lhsPresentationData, lhsInfo): - if case let .StorageInfo(rhsPresentationData, rhsInfo) = rhs { + case let .Notice(lhsPresentationData, lhsInfo): + if case let .Notice(rhsPresentationData, rhsInfo) = rhs { if lhsPresentationData !== rhsPresentationData { return false } @@ -399,7 +404,7 @@ private func offsetPinnedIndex(_ index: EngineChatList.Item.Index, offset: UInt1 } } -func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, storageInfo: Double?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation) -> (entries: [ChatListNodeEntry], loading: Bool) { +func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, storageInfo: Double?, suggestPasswordSetup: Bool, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation) -> (entries: [ChatListNodeEntry], loading: Bool) { var result: [ChatListNodeEntry] = [] var pinnedIndexOffset: UInt16 = 0 @@ -661,8 +666,10 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState if displayArchiveIntro { result.append(.ArchiveIntro(presentationData: state.presentationData)) } - if let storageInfo { - result.append(.StorageInfo(presentationData: state.presentationData, sizeFraction: storageInfo)) + if suggestPasswordSetup { + result.append(.Notice(presentationData: state.presentationData, notice: .setupPassword)) + } else if let storageInfo { + result.append(.Notice(presentationData: state.presentationData, notice: .clearStorage(sizeFraction: storageInfo))) } result.append(.HeaderEntry) diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index 0572ee0264..dc20cb0993 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -11,15 +11,15 @@ import AppBundle class ChatListStorageInfoItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings - let sizeFraction: Double + let notice: ChatListNotice let action: () -> Void let selectable: Bool = true - init(theme: PresentationTheme, strings: PresentationStrings, sizeFraction: Double, action: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping () -> Void) { self.theme = theme self.strings = strings - self.sizeFraction = sizeFraction + self.notice = notice self.action = action } @@ -117,23 +117,37 @@ class ChatListStorageInfoItemNode: ListViewItemNode { let _ = baseWidth let sideInset: CGFloat = params.leftInset + 16.0 - let height: CGFloat = 54.0 let rightInset: CGFloat = sideInset + 24.0 + let verticalInset: CGFloat = 8.0 + let spacing: CGFloat = 0.0 let themeUpdated = item.theme !== previousItem?.theme - let sizeString = dataSizeString(Int64(item.sizeFraction), formatting: DataSizeStringFormatting(strings: item.strings, decimalSeparator: ".")) - let rawTitleString = item.strings.ChatList_StorageHintTitle(sizeString) - let titleString = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) - if let range = rawTitleString.ranges.first { - titleString.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range) + let titleString: NSAttributedString + let textString: NSAttributedString + + switch item.notice { + case let .clearStorage(sizeFraction): + let sizeString = dataSizeString(Int64(sizeFraction), formatting: DataSizeStringFormatting(strings: item.strings, decimalSeparator: ".")) + let rawTitleString = item.strings.ChatList_StorageHintTitle(sizeString) + let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) + if let range = rawTitleString.ranges.first { + titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range) + } + titleString = titleStringValue + + textString = NSAttributedString(string: item.strings.ChatList_StorageHintText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + case .setupPassword: + //TODO:localize + titleString = NSAttributedString(string: "Protect Your Account", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) + textString = NSAttributedString(string: "Set a password that will be required each time you log in with this phone number.", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) } let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) - let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_StorageHintText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) + let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) - let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: height), insets: UIEdgeInsets()) + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.0.size.height + textLayout.0.size.height), insets: UIEdgeInsets()) return (layout, { [weak self] in if let strongSelf = self { @@ -148,10 +162,10 @@ class ChatListStorageInfoItemNode: ListViewItemNode { strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)) let _ = titleLayout.1() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 9.0), size: titleLayout.0.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleLayout.0.size) let _ = textLayout.1() - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: strongSelf.titleNode.frame.maxY - 0.0), size: textLayout.0.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size) if let image = strongSelf.arrowNode.image { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size) diff --git a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift index 3e75a2d684..7624c5ad3f 100644 --- a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift +++ b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift @@ -123,7 +123,7 @@ public final class MultilineTextComponent: Component { attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes) } - let previousText = self.attributedText?.string + //let previousText = self.attributedText?.string self.attributedText = attributedString self.maximumNumberOfLines = component.maximumNumberOfLines @@ -140,7 +140,7 @@ public final class MultilineTextComponent: Component { self.tapAttributeAction = component.tapAction self.longTapAttributeAction = component.longTapAction - if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string { + /*if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string { if let snapshotView = self.snapshotView(afterScreenUpdates: false) { snapshotView.center = self.center self.superview?.addSubview(snapshotView) @@ -150,7 +150,7 @@ public final class MultilineTextComponent: Component { }) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) } - } + }*/ let size = self.updateLayout(availableSize) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 4269a0d8e4..dae69edd03 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -92,6 +92,7 @@ public final class HashtagSearchController: TelegramBaseController { }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: { + }, openPasswordSetup: { }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index 98eb144214..6cfb1b8b14 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -660,11 +660,18 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da return storageUsageExceptionsScreen(context: context, category: category) })) }, openNetworkUsage: { - //pushControllerImpl?(networkUsageStatsController(context: context)) - let _ = (accountNetworkUsageStats(account: context.account, reset: []) |> take(1) |> deliverOnMainQueue).start(next: { stats in + var stats = stats + + if stats.resetWifiTimestamp == 0 { + var value = stat() + if stat(context.account.basePath, &value) == 0 { + stats.resetWifiTimestamp = Int32(value.st_ctimespec.tv_sec) + } + } + pushControllerImpl?(DataUsageScreen(context: context, stats: stats)) }) }, openProxy: { diff --git a/submodules/SettingsUI/Sources/SettingsController.swift b/submodules/SettingsUI/Sources/SettingsController.swift index 71484a7b62..dfc7d821a3 100644 --- a/submodules/SettingsUI/Sources/SettingsController.swift +++ b/submodules/SettingsUI/Sources/SettingsController.swift @@ -5,6 +5,7 @@ import Display import Postbox import TelegramCore import AccountContext +import PasswordSetupUI public protocol SettingsController: AnyObject { func updateContext(context: AccountContext) @@ -13,3 +14,14 @@ public protocol SettingsController: AnyObject { public func makePrivacyAndSecurityController(context: AccountContext) -> ViewController { return privacyAndSecurityController(context: context, focusOnItemTag: PrivacyAndSecurityEntryTag.autoArchive) } + +public func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController { + 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 + ))) + return controller +} diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index b5fa7159d1..8c1dc7adde 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -222,7 +222,8 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: { + }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 2741d9e64c..ca00127cf6 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -844,7 +844,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate gesture?.cancel() }, present: { _ in }, openForumThread: { _, _ in }, - openStorageManagement: {}) + openStorageManagement: {}, openPasswordSetup: { + }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) func makeChatListItem( diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index a29dc13b84..f5aa1d5990 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -367,7 +367,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}) + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: { + }) func makeChatListItem( peer: EnginePeer, diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index e0f226d281..60c1dee9ba 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -234,21 +234,17 @@ public struct NetworkUsageStatsConnectionsEntry: Equatable { } public struct NetworkUsageStats: Equatable { - public let generic: NetworkUsageStatsConnectionsEntry - public let image: NetworkUsageStatsConnectionsEntry - public let video: NetworkUsageStatsConnectionsEntry - public let audio: NetworkUsageStatsConnectionsEntry - public let file: NetworkUsageStatsConnectionsEntry - public let call: NetworkUsageStatsConnectionsEntry - public let sticker: NetworkUsageStatsConnectionsEntry - public let voiceMessage: NetworkUsageStatsConnectionsEntry + public var generic: NetworkUsageStatsConnectionsEntry + public var image: NetworkUsageStatsConnectionsEntry + public var video: NetworkUsageStatsConnectionsEntry + public var audio: NetworkUsageStatsConnectionsEntry + public var file: NetworkUsageStatsConnectionsEntry + public var call: NetworkUsageStatsConnectionsEntry + public var sticker: NetworkUsageStatsConnectionsEntry + public var voiceMessage: NetworkUsageStatsConnectionsEntry - public let resetWifiTimestamp: Int32 - public let resetCellularTimestamp: Int32 - - public static func ==(lhs: NetworkUsageStats, rhs: NetworkUsageStats) -> Bool { - return lhs.generic == rhs.generic && lhs.image == rhs.image && lhs.video == rhs.video && lhs.audio == rhs.audio && lhs.file == rhs.file && lhs.call == rhs.call && lhs.resetWifiTimestamp == rhs.resetWifiTimestamp && lhs.resetCellularTimestamp == rhs.resetCellularTimestamp && lhs.sticker == rhs.sticker && lhs.voiceMessage == rhs.voiceMessage - } + public var resetWifiTimestamp: Int32 + public var resetCellularTimestamp: Int32 } public struct ResetNetworkUsageStats: OptionSet { diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index e8e81c25e4..523dd5cb09 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -8,6 +8,7 @@ public enum ServerProvidedSuggestion: String { case newcomerTicks = "NEWCOMER_TICKS" case validatePhoneNumber = "VALIDATE_PHONE_NUMBER" case validatePassword = "VALIDATE_PASSWORD" + case setupPassword = "SETUP_2FA" } private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set]>([:]) @@ -28,9 +29,17 @@ public func getServerProvidedSuggestions(account: Account) -> Signal<[ServerProv guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else { return [] } - guard let data = appConfiguration.data, let list = data["pending_suggestions"] as? [String] else { + guard let data = appConfiguration.data, let listItems = data["pending_suggestions"] as? [String] else { return [] } + + #if DEBUG + var list = listItems + list.append(ServerProvidedSuggestion.setupPassword.rawValue) + #else + let list = listItems + #endif + return list.compactMap { item -> ServerProvidedSuggestion? in return ServerProvidedSuggestion(rawValue: item) }.filter { !dismissedSuggestions.contains($0) } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoriesComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoriesComponent.swift index 150ac91f3b..f3166b2463 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoriesComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataCategoriesComponent.swift @@ -175,6 +175,7 @@ final class DataCategoriesComponent: Component { } self.backgroundColor = component.theme.list.itemBlocksBackgroundColor + self.containerView.backgroundColor = component.theme.list.itemBlocksBackgroundColor self.containerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: contentHeight)) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift index a180f69f3c..f120f422c7 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift @@ -168,7 +168,7 @@ final class DataUsageScreenComponent: Component { init(stats: NetworkUsageStats) { self.wifi = Stats(stats: stats, isWifi: true) self.cellular = Stats(stats: stats, isWifi: false) - self.resetTimestamp = stats.resetWifiTimestamp + self.resetTimestamp = max(stats.resetWifiTimestamp, stats.resetCellularTimestamp) } } @@ -293,7 +293,8 @@ final class DataUsageScreenComponent: Component { private let headerOffsetContainer: UIView private let headerDescriptionView = ComponentView() - private var doneStatusNode: RadialStatusNode? + private var doneLabel: ComponentView? + private var doneSupLabel: ComponentView? private let scrollContainerView: UIView @@ -566,8 +567,17 @@ final class DataUsageScreenComponent: Component { chartItems.append(PieChartComponent.ChartData.Item(id: AnyHashable(listCategory.key), displayValue: listCategory.sizeFraction, displaySize: listCategory.size, value: categoryChartFraction, color: listCategory.color, particle: nil, title: listCategory.key.title(strings: environment.strings), mergeable: false, mergeFactor: 1.0)) } + var emptyValue: CGFloat = 0.0 if totalSize == 0 { + for i in 0 ..< chartItems.count { + chartItems[i].value = 0.0 + } + emptyValue = 1.0 + } + if let allStats = self.allStats, allStats.wifi.isEmpty && allStats.cellular.isEmpty { chartItems.removeAll() + } else { + chartItems.append(PieChartComponent.ChartData.Item(id: "empty", displayValue: 0.0, displaySize: 0, value: emptyValue, color: UIColor(rgb: 0xC4C4C6), particle: nil, title: "", mergeable: false, mergeFactor: 1.0)) } let totalCategories: [DataCategoriesComponent.CategoryData] = [ @@ -611,6 +621,7 @@ final class DataUsageScreenComponent: Component { component: AnyComponent(PieChartComponent( theme: environment.theme, strings: environment.strings, + emptyColor: environment.theme.list.itemAccentColor, chartData: chartData )), environment: {}, @@ -625,42 +636,75 @@ final class DataUsageScreenComponent: Component { pieChartTransition.setFrame(view: pieChartComponentView, frame: pieChartFrame) } if let allStats = self.allStats, allStats.wifi.isEmpty && allStats.cellular.isEmpty { - let checkColor = UIColor(rgb: 0x34C759) + let checkColor = environment.theme.list.itemAccentColor - let doneStatusNode: RadialStatusNode - var animateIn = false - if let current = self.doneStatusNode { - doneStatusNode = current + var doneLabelTransition = transition + let doneLabel: ComponentView + if let current = self.doneLabel { + doneLabel = current } else { - doneStatusNode = RadialStatusNode(backgroundNodeColor: .clear) - self.doneStatusNode = doneStatusNode - self.scrollView.addSubnode(doneStatusNode) - animateIn = true - } - let doneSize = CGSize(width: 100.0, height: 100.0) - doneStatusNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - doneSize.width) / 2.0), y: contentHeight), size: doneSize) - - if animateIn { - Queue.mainQueue().after(0.18, { - doneStatusNode.transitionToState(.check(checkColor), animated: true) - }) + doneLabelTransition = .immediate + doneLabel = ComponentView() + self.doneLabel = doneLabel } - contentHeight += doneSize.height + let doneSupLabel: ComponentView + if let current = self.doneSupLabel { + doneSupLabel = current + } else { + doneSupLabel = ComponentView() + self.doneSupLabel = doneSupLabel + } + + let doneLabelSize = doneLabel.update(transition: doneLabelTransition, component: AnyComponent(Text(text: "0", font: UIFont.systemFont(ofSize: 50.0, weight: UIFont.Weight(0.25)), color: checkColor)), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0)) + let doneLabelFrame = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - doneLabelSize.width) * 0.5), y: pieChartFrame.minY + 16.0), size: doneLabelSize) + if let doneLabelView = doneLabel.view { + var animateIn = false + if doneLabelView.superview == nil { + self.scrollView.addSubview(doneLabelView) + animateIn = true + } + doneLabelTransition.setFrame(view: doneLabelView, frame: doneLabelFrame) + + if animateIn { + doneLabelView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) + } + } + + let doneSupLabelSize = doneSupLabel.update(transition: doneLabelTransition, component: AnyComponent(Text(text: "KB", font: avatarPlaceholderFont(size: 12.0), color: checkColor)), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0)) + let doneSupLabelFrame = CGRect(origin: CGPoint(x: doneLabelFrame.maxX + 1.0, y: doneLabelFrame.minY + 10.0), size: doneSupLabelSize) + if let doneSupLabelView = doneSupLabel.view { + var animateIn = false + if doneSupLabelView.superview == nil { + self.scrollView.addSubview(doneSupLabelView) + animateIn = true + } + doneLabelTransition.setFrame(view: doneSupLabelView, frame: doneSupLabelFrame) + + if animateIn { + doneSupLabelView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) + } + } + + contentHeight += 100.0 } else { contentHeight += pieChartSize.height - if let doneStatusNode = self.doneStatusNode { - self.doneStatusNode = nil - doneStatusNode.removeFromSupernode() + if let doneLabel = self.doneLabel { + self.doneLabel = nil + doneLabel.view?.removeFromSuperview() + } + if let doneSupLabel = self.doneSupLabel { + self.doneSupLabel = nil + doneSupLabel.view?.removeFromSuperview() } } contentHeight += 23.0 let headerText: String - if listCategories.isEmpty { - headerText = "Data Usage Reset" + if totalSize == 0 { + headerText = "No Data Used" } else { headerText = "Data Usage" } @@ -686,15 +730,19 @@ final class DataUsageScreenComponent: Component { let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor) //TODO:localize - let timestampString: String if let allStats = self.allStats, allStats.resetTimestamp != 0 { - let formatter = DateFormatter() - formatter.dateFormat = "E, d MMM yyyy HH:mm" - let dateStringPlain = formatter.string(from: Date(timeIntervalSince1970: Double(allStats.resetTimestamp))) - timestampString = "Your network usage since \(dateStringPlain)" + let dateStringPlain = stringForFullDate(timestamp: allStats.resetTimestamp, strings: environment.strings, dateTimeFormat: PresentationDateTimeFormat()) + switch self.selectedStats { + case .all: + timestampString = "Your data usage since \(dateStringPlain)" + case .mobile: + timestampString = "Your mobile data usage since \(dateStringPlain)" + case .wifi: + timestampString = "Your Wi-Fi data usage since \(dateStringPlain)" + } } else { - timestampString = "Your network usage" + timestampString = "" } let totalUsageText: String = timestampString @@ -743,8 +791,13 @@ final class DataUsageScreenComponent: Component { animatedTextItems.append(AnimatedTextComponent.Item(id: "rest", isUnbreakable: true, content: .text(remainingSizeText))) } + var labelTransition = transition + if labelTransition.animation.isImmediate, let animationHint, animationHint.value == .modeChanged { + labelTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) + } + let chartTotalLabelSize = self.chartTotalLabel.update( - transition: transition, + transition: labelTransition, component: AnyComponent(AnimatedTextComponent( font: Font.with(size: 20.0, design: .round, weight: .bold), color: environment.theme.list.itemPrimaryTextColor, @@ -758,8 +811,8 @@ final class DataUsageScreenComponent: Component { self.scrollContainerView.addSubview(chartTotalLabelView) } let totalLabelFrame = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - chartTotalLabelSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - chartTotalLabelSize.height) / 2.0)), size: chartTotalLabelSize) - transition.setFrame(view: chartTotalLabelView, frame: totalLabelFrame) - transition.setAlpha(view: chartTotalLabelView, alpha: listCategories.isEmpty ? 0.0 : 1.0) + labelTransition.setFrame(view: chartTotalLabelView, frame: totalLabelFrame) + labelTransition.setAlpha(view: chartTotalLabelView, alpha: listCategories.isEmpty ? 0.0 : 1.0) } } @@ -848,9 +901,18 @@ final class DataUsageScreenComponent: Component { contentHeight += 40.0 //TODO:localize + let totalTitle: String + switch self.selectedStats { + case .all: + totalTitle = "TOTAL NETWORK USAGE" + case .mobile: + totalTitle = "MOBILE NETWORK USAGE" + case .wifi: + totalTitle = "WI-FI NETWORK USAGE" + } let totalCategoriesTitleSize = self.totalCategoriesTitleView.update( transition: transition, - component: AnyComponent(MultilineTextComponent(text: .markdown(text: "TOTAL NETWORK USAGE", attributes: MarkdownAttributes( + component: AnyComponent(MultilineTextComponent(text: .markdown(text: totalTitle, attributes: MarkdownAttributes( body: body, bold: bold, link: body, diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index 543ed5eec2..8ffb2a11dc 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -242,15 +242,18 @@ final class PieChartComponent: Component { let theme: PresentationTheme let strings: PresentationStrings + let emptyColor: UIColor let chartData: ChartData init( theme: PresentationTheme, strings: PresentationStrings, + emptyColor: UIColor, chartData: ChartData ) { self.theme = theme self.strings = strings + self.emptyColor = emptyColor self.chartData = chartData } @@ -261,6 +264,9 @@ final class PieChartComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.emptyColor != rhs.emptyColor { + return false + } if lhs.chartData != rhs.chartData { return false } @@ -366,6 +372,8 @@ final class PieChartComponent: Component { var label: CalculatedLabel? if let leftLabel = left.label, let rightLabel = right.label { label = leftLabel.interpolateTo(rightLabel, amount: progress) + } else { + label = right.label } self.sections.append(CalculatedSection( @@ -385,7 +393,7 @@ final class PieChartComponent: Component { } } - init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?, isEmpty: Bool) { + init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?, isEmpty: Bool, emptyColor: UIColor) { self.size = size self.sections = [] self.isEmpty = isEmpty @@ -447,7 +455,7 @@ final class PieChartComponent: Component { var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle) - let itemColor: UIColor = isEmpty ? UIColor(rgb: 0x34C759) : item.color + let itemColor: UIColor = isEmpty ? emptyColor : item.color self.sections.append(CalculatedSection( id: item.id, @@ -504,15 +512,17 @@ final class PieChartComponent: Component { let fractionValue: Double = floor(displayValue * 100.0 * 10.0) / 10.0 let fractionString: String - if fractionValue < 0.1 { - fractionString = "<0.1" + if fractionValue == 0.0 { + fractionString = "" + } else if fractionValue < 0.1 { + fractionString = "<0.1%" } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 { - fractionString = "\(Int(fractionValue))" + fractionString = "\(Int(fractionValue))%" } else { - fractionString = "\(fractionValue)" + fractionString = "\(fractionValue)%" } - let labelString = NSAttributedString(string: "\(fractionString)%", font: chartLabelFont, textColor: .white) + let labelString = NSAttributedString(string: fractionString, font: chartLabelFont, textColor: .white) let labelBounds = labelString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height)) guard let labelImage = generateImage(labelSize, rotatedContext: { size, context in @@ -922,12 +932,15 @@ final class PieChartComponent: Component { } private final class DoneLayer: SimpleLayer { + private let particleColor: UIColor private let maskShapeLayer: CAShapeLayer private var particleImage: UIImage? private var particleSet: ParticleSet? private var particleLayers: [SimpleLayer] = [] - override init() { + init(particleColor: UIColor) { + self.particleColor = particleColor + self.maskShapeLayer = CAShapeLayer() self.maskShapeLayer.fillColor = UIColor.black.cgColor self.maskShapeLayer.fillRule = .evenOdd @@ -948,6 +961,7 @@ final class PieChartComponent: Component { } override init(layer: Any) { + self.particleColor = .white self.maskShapeLayer = CAShapeLayer() super.init(layer: layer) @@ -982,7 +996,7 @@ final class PieChartComponent: Component { self.particleLayers.append(particleLayer) self.addSublayer(particleLayer) - particleLayer.layerTintColor = UIColor(rgb: 0x34C759).cgColor + particleLayer.layerTintColor = self.particleColor.cgColor } particleLayer.position = particle.position @@ -1010,6 +1024,7 @@ final class PieChartComponent: Component { private final class ChartDataView: UIView { private(set) var theme: PresentationTheme? private(set) var data: ChartData? + private var emptyColor: UIColor? private(set) var selectedKey: AnyHashable? private var currentAnimation: (start: CalculatedLayout, startTime: Double, duration: Double)? @@ -1077,7 +1092,9 @@ final class PieChartComponent: Component { return nil } - func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) { + func setItems(theme: PresentationTheme, emptyColor: UIColor, data: ChartData, selectedKey: AnyHashable?, animated: Bool) { + self.emptyColor = emptyColor + let data = processChartData(data: data) if self.theme !== theme || self.data != data || self.selectedKey != selectedKey { @@ -1099,14 +1116,16 @@ final class PieChartComponent: Component { size: CGSize(width: 200.0, height: 200.0), items: previousData.items, selectedKey: self.selectedKey, - isEmpty: true + isEmpty: true, + emptyColor: emptyColor ) } else { targetLayout = CalculatedLayout( size: CGSize(width: 200.0, height: 200.0), items: data.items, selectedKey: self.selectedKey, - isEmpty: false + isEmpty: false, + emptyColor: emptyColor ) } @@ -1118,14 +1137,16 @@ final class PieChartComponent: Component { size: CGSize(width: 200.0, height: 200.0), items: [.init(id: AnyHashable(StorageUsageScreenComponent.Category.other), displayValue: 0.0, displaySize: 0, value: 1.0, color: .green, particle: "Settings/Storage/ParticleOther", title: "", mergeable: false, mergeFactor: 1.0)], selectedKey: self.selectedKey, - isEmpty: true + isEmpty: true, + emptyColor: emptyColor ) } else { self.currentLayout = CalculatedLayout( size: CGSize(width: 200.0, height: 200.0), items: data.items, selectedKey: self.selectedKey, - isEmpty: data.items.isEmpty + isEmpty: data.items.isEmpty, + emptyColor: emptyColor ) } } @@ -1140,7 +1161,7 @@ final class PieChartComponent: Component { self.particleSet.update(deltaTime: deltaTime) var validIds: [AnyHashable] = [] - if let currentLayout = self.currentLayout { + if let currentLayout = self.currentLayout, let emptyColor = self.emptyColor { var effectiveLayout = currentLayout var verticalOffset: CGFloat = 0.0 var particleAlpha: CGFloat = 1.0 @@ -1195,7 +1216,7 @@ final class PieChartComponent: Component { if let current = self.doneLayer { doneLayer = current } else { - doneLayer = DoneLayer() + doneLayer = DoneLayer(particleColor: emptyColor) self.doneLayer = doneLayer self.layer.insertSublayer(doneLayer, at: 0) } @@ -1269,7 +1290,7 @@ final class PieChartComponent: Component { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let point = recognizer.location(in: self.dataView) - if let key = self.dataView.sectionKey(at: point) { + if let key = self.dataView.sectionKey(at: point), key != AnyHashable("empty") { if self.selectedKey == key { self.selectedKey = nil } else { @@ -1293,7 +1314,7 @@ final class PieChartComponent: Component { } transition.setFrame(view: self.dataView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - 200.0) / 2.0), y: 0.0), size: CGSize(width: 200.0, height: 200.0))) - self.dataView.setItems(theme: component.theme, data: component.chartData, selectedKey: self.selectedKey, animated: !transition.animation.isImmediate) + self.dataView.setItems(theme: component.theme, emptyColor: component.emptyColor, data: component.chartData, selectedKey: self.selectedKey, animated: !transition.animation.isImmediate) if let selectedKey = self.selectedKey, let item = component.chartData.items.first(where: { $0.id == selectedKey }) { let tooltip: ComponentView diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 0a929020f8..91331ae486 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -1392,6 +1392,7 @@ final class StorageUsageScreenComponent: Component { component: AnyComponent(PieChartComponent( theme: environment.theme, strings: environment.strings, + emptyColor: UIColor(rgb: 0x34C759), chartData: chartData )), environment: {}, @@ -1404,7 +1405,6 @@ final class StorageUsageScreenComponent: Component { } pieChartTransition.setFrame(view: pieChartComponentView, frame: pieChartFrame) - //transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0) } if let _ = self.aggregatedData, listCategories.isEmpty { let checkColor = UIColor(rgb: 0x34C759) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index 65d1497013..024e7df06a 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -79,8 +79,15 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa } switch inputQueryResult { - case let .stickers(results): - if !results.isEmpty { + case let .stickers(unfilteredResults): + if !unfilteredResults.isEmpty { + var results: [FoundStickerItem] = [] + for result in unfilteredResults { + if !results.contains(where: { $0.file.fileId == result.file.fileId }) { + results.append(result) + } + } + let query = chatPresentationInterfaceState.interfaceState.composeInputState.inputText.string if let currentPanel = currentPanel as? InlineReactionSearchPanel { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index dc21c182e1..3f0e119192 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -1122,7 +1122,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if file.isAnimated { strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: file), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).start()) } else { - strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual).start()) + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual, storeToDownloadsPeerType: storeToDownloadsPeerType).start()) } } }, cancel: { diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 45eed334bd..e15da45c23 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -263,6 +263,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: { + }, openPasswordSetup: { }) interaction.searchTextHighightState = searchQuery self.interaction = interaction diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index 206becc8db..419a87926a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -112,6 +112,7 @@ final class PeerInfoState { final class TelegramGlobalSettings { let suggestPhoneNumberConfirmation: Bool let suggestPasswordConfirmation: Bool + let suggestPasswordSetup: Bool let accountsAndPeers: [(AccountContext, EnginePeer, Int32)] let activeSessionsContext: ActiveSessionsContext? let webSessionsContext: WebSessionsContext? @@ -132,6 +133,7 @@ final class TelegramGlobalSettings { init( suggestPhoneNumberConfirmation: Bool, suggestPasswordConfirmation: Bool, + suggestPasswordSetup: Bool, accountsAndPeers: [(AccountContext, EnginePeer, Int32)], activeSessionsContext: ActiveSessionsContext?, webSessionsContext: WebSessionsContext?, @@ -151,6 +153,7 @@ final class TelegramGlobalSettings { ) { self.suggestPhoneNumberConfirmation = suggestPhoneNumberConfirmation self.suggestPasswordConfirmation = suggestPasswordConfirmation + self.suggestPasswordSetup = suggestPasswordSetup self.accountsAndPeers = accountsAndPeers self.activeSessionsContext = activeSessionsContext self.webSessionsContext = webSessionsContext @@ -409,6 +412,23 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, ) } + let hasPassword: Signal = .single(nil) |> then( + context.engine.auth.twoStepVerificationConfiguration() + |> map { configuration -> Bool? in + var notSet = false + switch configuration { + case let .notSet(pendingEmail): + if pendingEmail == nil { + notSet = true + } + case .set: + break + } + return !notSet + } + ) + |> distinctUntilChanged + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), accountsAndPeers, @@ -424,9 +444,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) - ) + ), + hasPassword ) - |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits -> PeerInfoScreenData in + |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword -> PeerInfoScreenData in let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications let (featuredStickerPacks, archivedStickerPacks) = stickerPacks @@ -443,10 +464,16 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, enableQRLogin = true } + var suggestPasswordSetup = false + if suggestions.contains(.setupPassword), let hasPassword, !hasPassword { + suggestPasswordSetup = true + } + let peer = peerView.peers[peerId] let globalSettings = TelegramGlobalSettings( suggestPhoneNumberConfirmation: suggestions.contains(.validatePhoneNumber), suggestPasswordConfirmation: suggestions.contains(.validatePassword), + suggestPasswordSetup: suggestPasswordSetup, accountsAndPeers: accountsAndPeers, activeSessionsContext: accountSessions?.0, webSessionsContext: accountSessions?.2, diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index d7c8e7637b..faaaf3348c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -453,6 +453,7 @@ private enum PeerInfoSettingsSection { case chatFolders case notificationsAndSounds case privacyAndSecurity + case passwordSetup case dataAndStorage case appearance case language @@ -720,6 +721,13 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: presentationData.strings.Settings_TryEnterPassword, action: { interaction.openSettings(.rememberPassword) })) + } else if settings.suggestPasswordSetup { + //TODO:localize + items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: "Protect Your Account", text: .markdown("Set a password that will be required each time log in with this phone number."), linkAction: { _ in + })) + items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: "Set Additional Password", action: { + interaction.openSettings(.passwordSetup) + })) } if !settings.accountsAndPeers.isEmpty { @@ -7455,6 +7463,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } }) } + case .passwordSetup: + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in + guard let self else { + return + } + let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).start() + }) + + let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context) + push(controller) case .dataAndStorage: push(dataAndStorageController(context: self.context)) case .appearance: diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 54bfd835e6..64235f274f 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1425,6 +1425,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return SettingsUI.makePrivacyAndSecurityController(context: context) } + public func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController { + return SettingsUI.makeSetupTwoFactorAuthController(context: context) + } + public func makeStorageManagementController(context: AccountContext) -> ViewController { return StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { [weak context] category in guard let context else {