From 63c2357baa1aef09d850cb8a85ec0f466a3e71bb Mon Sep 17 00:00:00 2001 From: mvsrcdst <43897151+mvsrcdst@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:59:59 +0300 Subject: [PATCH] feat: add ability to hide folders --- .../Sources/SGHiddenFolders.swift | 96 ++++++ .../Sources/SimpleSettings.swift | 15 + .../Strings/en.lproj/SGLocalizable.strings | 6 + submodules/ChatListUI/BUILD | 3 +- .../Sources/ChatListController.swift | 300 +++++++++++------- .../Sources/ChatListControllerNode.swift | 4 +- .../ChatListFilterPresetListController.swift | 55 +++- .../ChatListFilterPresetListItem.swift | 28 +- 8 files changed, 373 insertions(+), 134 deletions(-) create mode 100644 Swiftgram/SGChatListSimpleSettingsSignal/Sources/SGHiddenFolders.swift diff --git a/Swiftgram/SGChatListSimpleSettingsSignal/Sources/SGHiddenFolders.swift b/Swiftgram/SGChatListSimpleSettingsSignal/Sources/SGHiddenFolders.swift new file mode 100644 index 00000000000..9b3604b4e9b --- /dev/null +++ b/Swiftgram/SGChatListSimpleSettingsSignal/Sources/SGHiddenFolders.swift @@ -0,0 +1,96 @@ +import Foundation +import UIKit +import Display +import TelegramCore +import ContextUI +import AppBundle +import SwiftSignalKit +import SGSimpleSettings +import SGStrings + +public func sgHiddenChatListFilterIdsSignal() -> Signal, NoError> { + let key = SGSimpleSettings.Keys.hiddenChatListFilterIds + let read: () -> Set = { + let raw = UserDefaults.standard.stringArray(forKey: key.rawValue) ?? [] + return Set(raw.compactMap { Int32($0) }) + } + let initial = Signal, NoError>.single(read()) + let changes = Signal, NoError> { subscriber in + let observer = NotificationCenter.default.addObserver( + forName: UserDefaults.didChangeNotification, + object: UserDefaults.standard, + queue: nil + ) { _ in + subscriber.putNext(read()) + } + return ActionDisposable { + NotificationCenter.default.removeObserver(observer) + } + } + return (initial |> then(changes)) |> distinctUntilChanged +} + +public func sgVisibleFilters(_ filters: [ChatListContainerNodeFilter]) -> [ChatListContainerNodeFilter] { + var filters = filters + if SGSimpleSettings.shared.allChatsHidden { + filters.removeAll { $0 == .all } + } + let sgHiddenFilterIds = SGSimpleSettings.shared.hiddenChatListFilterIds + if !sgHiddenFilterIds.isEmpty { + filters.removeAll { + if case let .filter(filter) = $0, case let .filter(id, _, _, _) = filter { + return sgHiddenFilterIds.contains(id) + } + return false + } + } + return filters +} + +public func sgFilterBadge(for id: Int32, filterItems: [(ChatListFilter, Int, Bool)]) -> ContextMenuActionBadge? { + for item in filterItems { + if item.0.id == id && item.1 != 0 { + return ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive) + } + } + return nil +} + +public func sgHiddenFoldersDrillDownItems(presetList: [ChatListFilter], sgHiddenIds: Set, filterItems: [(ChatListFilter, Int, Bool)], backTitle: String, onBack: @escaping () -> Void, onSelect: @escaping (Int32) -> Void) -> [ContextMenuItem] { + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: backTitle, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, action: { _, _ in + onBack() + }))) + items.append(.separator) + + for case let .filter(id, title, _, _) in presetList where sgHiddenIds.contains(id) { + items.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, badge: sgFilterBadge(for: id, filterItems: filterItems), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + onSelect(id) + }))) + } + return items +} + +public func sgHiddenFoldersSummaryItem(presetList: [ChatListFilter], sgHiddenIds: Set, filterItems: [(ChatListFilter, Int, Bool)], baseLanguageCode: String, onTap: @escaping () -> Void) -> ContextMenuItem? { + var hiddenUnreadTotal = 0 + var hasHidden = false + for case let .filter(id, _, _, _) in presetList where sgHiddenIds.contains(id) { + hasHidden = true + for item in filterItems where item.0.id == id { + hiddenUnreadTotal += item.1 + } + } + guard hasHidden else { + return nil + } + return .action(ContextMenuActionItem(text: i18n("ContextMenu.HiddenFolders", baseLanguageCode), badge: hiddenUnreadTotal != 0 ? ContextMenuActionBadge(value: "\(hiddenUnreadTotal)", color: .accent) : nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Eye"), color: theme.contextMenu.primaryColor) + }, action: { _, _ in + onTap() + })) +} diff --git a/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift b/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift index 2db47deb514..5b954939be4 100644 --- a/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift +++ b/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift @@ -146,6 +146,7 @@ public class SGSimpleSettings { case allChatsTitleLengthOverride // case allChatsFolderPositionOverride case allChatsHidden + case hiddenChatListFilterIds case defaultEmojisFirst case messageDoubleTapActionOutgoing case wideChannelPosts @@ -304,6 +305,7 @@ public class SGSimpleSettings { Keys.allChatsTitleLengthOverride.rawValue: AllChatsTitleLengthOverride.none.rawValue, // Keys.allChatsFolderPositionOverride.rawValue: AllChatsFolderPositionOverride.none.rawValue Keys.allChatsHidden.rawValue: false, + Keys.hiddenChatListFilterIds.rawValue: [], Keys.defaultEmojisFirst.rawValue: false, Keys.messageDoubleTapActionOutgoing.rawValue: MessageDoubleTapAction.default.rawValue, Keys.wideChannelPosts.rawValue: false, @@ -510,6 +512,19 @@ public class SGSimpleSettings { @UserDefault(key: Keys.allChatsHidden.rawValue) public var allChatsHidden: Bool + // Stored as stringified Int32 filter ids (UserDefault wrapper has no Array case) + @UserDefault(key: Keys.hiddenChatListFilterIds.rawValue) + public var hiddenChatListFilterIdsRaw: [String] + + public var hiddenChatListFilterIds: Set { + get { + return Set(self.hiddenChatListFilterIdsRaw.compactMap { Int32($0) }) + } + set { + self.hiddenChatListFilterIdsRaw = newValue.map { String($0) } + } + } + @UserDefault(key: Keys.defaultEmojisFirst.rawValue) public var defaultEmojisFirst: Bool diff --git a/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings index 0383460b65f..a49b5021ea4 100644 --- a/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings +++ b/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings @@ -26,6 +26,9 @@ "Settings.Folders.AllChatsTitle.long" = "Long"; /* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ "Settings.Folders.AllChatsTitle.none" = "Default"; +"Settings.Folders.Hide" = "Hide"; +"Settings.Folders.Show" = "Show"; +"Settings.Folders.Hidden" = "Hidden"; "Settings.ChatList.Header" = "CHAT LIST"; @@ -118,6 +121,9 @@ "Settings.SmallReactions" = "Small Reactions"; "Settings.HideReactions" = "Hide Reactions"; +"ContextMenu.HideFolder" = "Hide Folder"; +"ContextMenu.ShowFolder" = "Show Folder"; +"ContextMenu.HiddenFolders" = "Hidden Folders"; "ContextMenu.SaveToCloud" = "Save to Cloud"; "ContextMenu.SelectFromUser" = "Select from Author"; diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index b8af55ca50c..dcc28f23cc5 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -3,7 +3,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") sgdeps = [ "//Swiftgram/SGSimpleSettings:SGSimpleSettings", "//Swiftgram/SGAPIWebSettings:SGAPIWebSettings", - "//Swiftgram/SGAPIToken:SGAPIToken" + "//Swiftgram/SGAPIToken:SGAPIToken", + "//Swiftgram/SGStrings:SGStrings" ] sgsrcs = [ "//Swiftgram/SGChatListSimpleSettingsSignal:SGChatListSimpleSettingsSignal", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 2277b0f9b93..f09f56c7981 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1,5 +1,6 @@ // MARK: Swiftgram import SGSimpleSettings +import SGStrings import Foundation import UIKit @@ -132,7 +133,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var badgeDisposable: Disposable? private var badgeIconDisposable: Disposable? - + + // MARK: Swiftgram + private var sgFoldersTabAtBottom: Bool = false + private var sgCurrentFilterId: Int32 = -2 + private var didAppear = false private var dismissSearchOnDisappear = false public var onDidAppear: (() -> Void)? @@ -410,21 +415,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top(adjustForTempInset: false)) case let .known(offset): // MARK: Swiftgram - let sgAllChatsHiddden = SGSimpleSettings.shared.allChatsHidden - var mainContainerNode_availableFilters = strongSelf.chatListDisplayNode.mainContainerNode.availableFilters - if sgAllChatsHiddden { - mainContainerNode_availableFilters.removeAll { $0 == .all } - } + let mainContainerNode_availableFilters = sgVisibleFilters(strongSelf.chatListDisplayNode.mainContainerNode.availableFilters) let isFirstFilter = strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter == mainContainerNode_availableFilters.first?.filter - + if offset <= ChatListNavigationBar.searchScrollHeight + 1.0 && strongSelf.chatListDisplayNode.inlineStackContainerNode != nil { strongSelf.setInlineChatList(location: nil) } else if offset <= ChatListNavigationBar.searchScrollHeight + 1.0 && !isFirstFilter { // MARK: Swiftgram - var effectiveContainerNode_availableFilters = strongSelf.chatListDisplayNode.mainContainerNode.availableFilters - if sgAllChatsHiddden { - effectiveContainerNode_availableFilters.removeAll { $0 == .all } - } + let effectiveContainerNode_availableFilters = sgVisibleFilters(strongSelf.chatListDisplayNode.mainContainerNode.availableFilters) let firstFilter = effectiveContainerNode_availableFilters.first ?? .all let targetTab: ChatListFilterTabEntryId switch firstFilter { @@ -776,6 +774,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if SGSimpleSettings.shared.lastAccountFolders[accountId] != switchingToFilterId { SGSimpleSettings.shared.lastAccountFolders[accountId] = switchingToFilterId } + // MARK: Swiftgram + let previousFilterId = strongSelf.sgCurrentFilterId + strongSelf.sgCurrentFilterId = switchingToFilterId + let sgHiddenFilterIds = SGSimpleSettings.shared.hiddenChatListFilterIds + if sgHiddenFilterIds.contains(previousFilterId) || sgHiddenFilterIds.contains(switchingToFilterId) { + strongSelf.reloadFilters() + } } } @@ -1284,6 +1289,35 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + // MARK: Swiftgram + let sgIsHidden = SGSimpleSettings.shared.hiddenChatListFilterIds.contains(id) + items.append(.action(ContextMenuActionItem(text: i18n(sgIsHidden ? "ContextMenu.ShowFolder" : "ContextMenu.HideFolder", self.presentationData.strings.baseLanguageCode), textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: sgIsHidden ? "Peer Info/ShowIcon" : "Chat/Context Menu/Eye"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, f in + c?.dismiss(completion: { + guard let self else { + return + } + var hiddenIds = SGSimpleSettings.shared.hiddenChatListFilterIds + if sgIsHidden { + hiddenIds.remove(id) + } else { + hiddenIds.insert(id) + } + SGSimpleSettings.shared.hiddenChatListFilterIds = hiddenIds + if !sgIsHidden && self.sgCurrentFilterId == id { + let target = sgVisibleFilters(self.chatListDisplayNode.mainContainerNode.availableFilters).first + switch target { + case .some(.all), .none: + self.selectTab(id: .all) + case let .some(.filter(filter)): + self.selectTab(id: .filter(filter.id)) + } + } + self.reloadFilters() + }) + }))) + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.ChatList_RemoveFolder, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in @@ -1341,7 +1375,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController override public func loadDisplayNode() { self.displayNode = ChatListControllerNode(context: self.context, location: self.location, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, controller: self) - + self.chatListDisplayNode.navigationBar = self.navigationBar self.chatListDisplayNode.requestDeactivateSearch = { [weak self] in @@ -3187,7 +3221,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.validLayout = layout self.updateLayout(layout: layout, transition: transition) - + if layout.inVoiceOver != wasInVoiceOver { self.chatListDisplayNode.scrollToTop() } @@ -4046,23 +4080,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } |> distinctUntilChanged + // MARK: Swiftgram + let hiddenFilterIdsSignal = sgHiddenChatListFilterIdsSignal() + self.filterDisposable.set((combineLatest(queue: .mainQueue(), displayTabsAtBottomSignal, filterItems, self.context.account.postbox.peerView(id: self.context.account.peerId), - self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false)) + self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false)), + hiddenFilterIdsSignal ) - |> deliverOnMainQueue).startStrict(next: { [weak self] displayTabsAtBottom, countAndFilterItems, peerView, limits in + |> deliverOnMainQueue).startStrict(next: { [weak self] displayTabsAtBottom, countAndFilterItems, peerView, limits, sgHiddenFilterIds in guard let strongSelf = self else { return } let isPremium = peerView.peers[peerView.peerId]?.isPremium strongSelf.isPremium = isPremium ?? false - + + // MARK: Swiftgram + strongSelf.sgFoldersTabAtBottom = displayTabsAtBottom + let (_, items) = countAndFilterItems var filterItems: [ChatListFilterTabEntry] = [] - + for (filter, unreadCount, hasUnmutedUnread) in items { switch filter { case .allChats: @@ -4072,6 +4113,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController filterItems.append(.all(unreadCount: 0)) } case let .filter(id, title, _, _): + // MARK: Swiftgram + if sgHiddenFilterIds.contains(id) && strongSelf.sgCurrentFilterId != id { + continue + } filterItems.append(.filter(id: id, text: title, unread: ChatListFilterTabEntryUnreadCount(value: unreadCount, hasUnmuted: hasUnmutedUnread))) } } @@ -4103,8 +4148,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController resetCurrentEntry = true if let tabContainerData = strongSelf.tabContainerData { var found = false - if let index = tabContainerData.0.firstIndex(where: { $0.id == selectedEntryId }) { - for i in (0 ..< index - 1).reversed() { + if let index = tabContainerData.0.firstIndex(where: { $0.id == selectedEntryId }), index > 0 { + for i in (0 ..< index).reversed() { if resolvedItems.contains(where: { $0.id == tabContainerData.0[i].id }) { selectedEntryId = tabContainerData.0[i].id found = true @@ -4113,10 +4158,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } if !found { - selectedEntryId = .all + selectedEntryId = resolvedItems.first?.id ?? .all } } else { - selectedEntryId = .all + // MARK: Swiftgram + selectedEntryId = resolvedItems.first?.id ?? .all } } let filtersLimit = isPremium == false ? limits.maxFoldersCount : nil @@ -4141,9 +4187,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } strongSelf.chatListDisplayNode.mainContainerNode.updateAvailableFilters(availableFilters, limit: filtersLimit) + // MARK: Swiftgram + var didSwitchDirectly = false + if isPremium == nil && items.isEmpty { strongSelf.mainReady.set(strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.ready) } else if !strongSelf.initializedFilters { + didSwitchDirectly = true if selectedEntryId != strongSelf.chatListDisplayNode.mainContainerNode.currentItemFilter { strongSelf.chatListDisplayNode.mainContainerNode.switchToFilter(id: selectedEntryId, animated: false, completion: { [weak self] in if let strongSelf = self { @@ -4170,7 +4220,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController firstUpdate?() } - if resetCurrentEntry { + if resetCurrentEntry && !didSwitchDirectly { strongSelf.selectTab(id: selectedEntryId, switchToChatsIfNeeded: false) } })) @@ -4225,7 +4275,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }) } - + private func readAllInFilter(id: Int32) { for filter in self.chatListDisplayNode.mainContainerNode.availableFilters { if case let .filter(filter) = filter, case let .filter(filterId, _, _, data) = filter, filterId == id { @@ -6355,106 +6405,134 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let (accountPeer, limits, _) = result let isPremium = accountPeer?.isPremium ?? false - + let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().startStandalone() let (_, filterItems) = filterItemsAndTotalCount - - var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: presetList.isEmpty ? strongSelf.presentationData.strings.ChatList_AddFolder : strongSelf.presentationData.strings.ChatList_EditFolders, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: presetList.isEmpty ? "Chat/Context Menu/Add" : "Chat/Context Menu/ItemList"), color: theme.contextMenu.primaryColor) - }, action: { c, f in - c?.dismiss(completion: { - guard let strongSelf = self else { - return - } - strongSelf.openFilterSettings() - }) - }))) - - if strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter != nil { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChatList_FolderAllChats, icon: { theme in - return nil - }, action: { c, f in - f(.dismissWithoutContent) - guard let strongSelf = self else { - return - } - strongSelf.selectTab(id: .all) - }))) - } - - if !presetList.isEmpty { - if presetList.count > 1 { - items.append(.separator) - } - var filterCount = 0 - for case let .filter(id, title, _, data) in presetList { - let filterType = chatListFilterType(data) - var badge: ContextMenuActionBadge? - var isDisabled = false - if !isPremium && filterCount >= limits.maxFoldersCount { - isDisabled = true - } - - for item in filterItems { - if item.0.id == id && item.1 != 0 { - badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive) + + // MARK: Swiftgram + let sgHiddenIds = SGSimpleSettings.shared.hiddenChatListFilterIds + let showingHiddenPromise = ValuePromise(false, ignoreRepeated: true) + + let itemsSignal: Signal = showingHiddenPromise.get() + |> map { showingHidden -> ContextController.Items in + var items: [ContextMenuItem] = [] + + // MARK: Swiftgram + if showingHidden { + return ContextController.Items(content: .list(sgHiddenFoldersDrillDownItems(presetList: presetList, sgHiddenIds: sgHiddenIds, filterItems: filterItems, backTitle: strongSelf.presentationData.strings.Common_Back, onBack: { + showingHiddenPromise.set(false) + }, onSelect: { id in + guard let strongSelf = self else { + return } - } - items.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, badge: badge, icon: { theme in - let imageName: String - if isDisabled { - imageName = "Chat/Context Menu/Lock" - } else { - switch filterType { - case .generic: - imageName = "Chat/Context Menu/List" - case .unmuted: - imageName = "Chat/Context Menu/Unmute" - case .unread: - imageName = "Chat/Context Menu/MarkAsUnread" - case .channels: - imageName = "Chat/Context Menu/Channels" - case .groups: - imageName = "Chat/Context Menu/Groups" - case .bots: - imageName = "Chat/Context Menu/Bots" - case .contacts: - imageName = "Chat/Context Menu/User" - case .nonContacts: - imageName = "Chat/Context Menu/UnknownUser" - } + strongSelf.selectTab(id: .filter(id)) + }))) + } + + items.append(.action(ContextMenuActionItem(text: presetList.isEmpty ? strongSelf.presentationData.strings.ChatList_AddFolder : strongSelf.presentationData.strings.ChatList_EditFolders, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: presetList.isEmpty ? "Chat/Context Menu/Add" : "Chat/Context Menu/ItemList"), color: theme.contextMenu.primaryColor) + }, action: { c, f in + c?.dismiss(completion: { + guard let strongSelf = self else { + return } - return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) - }, action: { _, f in + strongSelf.openFilterSettings() + }) + }))) + + if strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter != nil { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChatList_FolderAllChats, icon: { theme in + // MARK: Swiftgram + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Chats"), color: theme.contextMenu.primaryColor) + }, action: { c, f in f(.dismissWithoutContent) guard let strongSelf = self else { return } - if isDisabled { - let context = strongSelf.context - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.foldersCount, action: { - let controller = PremiumIntroScreen(context: context, source: .folders) - replaceImpl?(controller) - return true - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) + strongSelf.selectTab(id: .all) + }))) + } + + // MARK: Swiftgram + if let summaryItem = sgHiddenFoldersSummaryItem(presetList: presetList, sgHiddenIds: sgHiddenIds, filterItems: filterItems, baseLanguageCode: strongSelf.presentationData.strings.baseLanguageCode, onTap: { + showingHiddenPromise.set(true) + }) { + items.append(summaryItem) + } + + if !presetList.isEmpty { + if presetList.count > 1 { + items.append(.separator) + } + var filterCount = 0 + for case let .filter(id, title, _, data) in presetList { + // MARK: Swiftgram + if sgHiddenIds.contains(id) { + continue + } + let filterType = chatListFilterType(data) + var isDisabled = false + if !isPremium && filterCount >= limits.maxFoldersCount { + isDisabled = true + } + + items.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, badge: sgFilterBadge(for: id, filterItems: filterItems), icon: { theme in + let imageName: String + if isDisabled { + imageName = "Chat/Context Menu/Lock" + } else { + switch filterType { + case .generic: + imageName = "Chat/Context Menu/List" + case .unmuted: + imageName = "Chat/Context Menu/Unmute" + case .unread: + imageName = "Chat/Context Menu/MarkAsUnread" + case .channels: + imageName = "Chat/Context Menu/Channels" + case .groups: + imageName = "Chat/Context Menu/Groups" + case .bots: + imageName = "Chat/Context Menu/Bots" + case .contacts: + imageName = "Chat/Context Menu/User" + case .nonContacts: + imageName = "Chat/Context Menu/UnknownUser" + } } - if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController { - navigationController.pushViewController(controller) + return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + guard let strongSelf = self else { + return } - } else { - strongSelf.selectTab(id: .filter(id)) - } - }))) - - filterCount += 1 + if isDisabled { + let context = strongSelf.context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.foldersCount, action: { + let controller = PremiumIntroScreen(context: context, source: .folders) + replaceImpl?(controller) + return true + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController { + navigationController.pushViewController(controller) + } + } else { + strongSelf.selectTab(id: .filter(id)) + } + }))) + + filterCount += 1 + } } + + return ContextController.Items(content: .list(items)) } - - let controller = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .reference(ChatListTabBarContextReferenceContentSource(controller: strongSelf, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) + + let controller = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .reference(ChatListTabBarContextReferenceContentSource(controller: strongSelf, sourceView: sourceView)), items: itemsSignal, recognizer: nil, gesture: gesture) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index d16e8614125..780c4e5c35e 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1524,7 +1524,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var navigationHeaderPanels: AnyComponent? var tabs: AnyComponent? // MARK: Swiftgram if self.controller?.tabContainerData != nil || !panels.isEmpty { - if let tabContainerData = self.controller?.tabContainerData, tabContainerData.0.count > 1 { + // MARK: Swiftgram + let sgSomeFiltersHidden = SGSimpleSettings.shared.allChatsHidden || !SGSimpleSettings.shared.hiddenChatListFilterIds.isEmpty + if let tabContainerData = self.controller?.tabContainerData, tabContainerData.0.count > 1 || (tabContainerData.0.count == 1 && sgSomeFiltersHidden) { let folderFilterIndex: (ChatListFilterTabEntryId, [ChatListFilterTabEntry]) -> Int? = { id, entries in var index = 0 for entry in entries { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index c37f487cdb4..3bdfc179a11 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -13,25 +13,29 @@ import ChatListFilterSettingsHeaderItem import PremiumUI import UndoUI import ChatFolderLinkPreviewScreen +import SGSimpleSettings +import SGStrings private final class ChatListFilterPresetListControllerArguments { let context: AccountContext - + let addSuggestedPressed: (ChatFolderTitle, ChatListFilterData) -> Void let openPreset: (ChatListFilter) -> Void let addNew: () -> Void let setItemWithRevealedOptions: (Int32?, Int32?) -> Void let removePreset: (Int32) -> Void + let toggleHiddenPreset: (Int32) -> Void let updateDisplayTags: (Bool) -> Void let updateDisplayTagsLocked: () -> Void - - init(context: AccountContext, addSuggestedPressed: @escaping (ChatFolderTitle, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) { + + init(context: AccountContext, addSuggestedPressed: @escaping (ChatFolderTitle, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, toggleHiddenPreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) { self.context = context self.addSuggestedPressed = addSuggestedPressed self.openPreset = openPreset self.addNew = addNew self.setItemWithRevealedOptions = setItemWithRevealedOptions self.removePreset = removePreset + self.toggleHiddenPreset = toggleHiddenPreset self.updateDisplayTags = updateDisplayTags self.updateDisplayTagsLocked = updateDisplayTagsLocked } @@ -98,7 +102,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case suggestedPreset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilterData) case suggestedAddCustom(String) case listHeader(String) - case preset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool) + case preset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, isHidden: Bool, displayTags: Bool) case addItem(text: String, isEditing: Bool) case listFooter(String) case displayTags(Bool?) @@ -125,7 +129,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return 100 case .addItem: return 101 - case let .preset(index, _, _, _, _, _, _, _, _, _): + case let .preset(index, _, _, _, _, _, _, _, _, _, _): return 102 + index.value case .listFooter: return 1001 @@ -154,7 +158,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return .suggestedAddCustom case .listHeader: return .listHeader - case let .preset(_, _, _, preset, _, _, _, _, _, _): + case let .preset(_, _, _, preset, _, _, _, _, _, _, _): return .preset(preset.id) case .addItem: return .addItem @@ -188,7 +192,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { }) case let .listHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) - case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats, isDisabled, displayTags): + case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats, isDisabled, isHidden, displayTags): var resolvedColor: UIColor? if displayTags, case let .filter(_, _, _, data) = preset { let tagColor = data.color @@ -196,8 +200,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { resolvedColor = arguments.context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance).main } } - - return ChatListFilterPresetListItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: { + + return ChatListFilterPresetListItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, isHidden: isHidden, sectionId: self.section, action: { if isDisabled { arguments.addNew() } else { @@ -207,6 +211,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { arguments.setItemWithRevealedOptions(lhs, rhs) }, remove: { arguments.removePreset(preset.id) + }, toggleHidden: { + arguments.toggleHiddenPreset(preset.id) }) case let .addItem(text, isEditing): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: text, sectionId: self.section, height: .generic, editing: isEditing, action: { @@ -233,6 +239,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { private struct ChatListFilterPresetListControllerState: Equatable { var isEditing: Bool = false var revealedPreset: Int32? = nil + // MARK: Swiftgram + var sgHiddenFilterIdsRevision: Int = 0 } private func filtersWithAppliedOrder(filters: [(ChatListFilter, Int)], order: [Int32]?) -> [(ChatListFilter, Int)] { @@ -286,13 +294,18 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present if !filters.isEmpty || suggestedFilters.isEmpty { var folderCount = 0 + // MARK: Swiftgram + let sgHiddenFilterIds = SGSimpleSettings.shared.hiddenChatListFilterIds for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { if case .allChats = filter { - entries.append(.preset(index: PresetIndex(value: entries.count), title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true)) + entries.append(.preset(index: PresetIndex(value: entries.count), title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, isHidden: false, displayTags: effectiveDisplayTags == true)) } - if case let .filter(_, title, _, _) = filter { + if case let .filter(id, title, _, _) = filter { folderCount += 1 - entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true)) + // MARK: Swiftgram + let isHidden = sgHiddenFilterIds.contains(id) + let label = isHidden ? "Settings.Folders.Hidden".i18n(presentationData.strings.baseLanguageCode) : (chatCount == 0 ? "" : "\(chatCount)") + entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: label, preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, isHidden: isHidden, displayTags: effectiveDisplayTags == true)) } } @@ -556,6 +569,20 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch presentControllerImpl?(alertController) } }) + }, toggleHiddenPreset: { id in + // MARK: Swiftgram + var hiddenIds = SGSimpleSettings.shared.hiddenChatListFilterIds + if hiddenIds.contains(id) { + hiddenIds.remove(id) + } else { + hiddenIds.insert(id) + } + SGSimpleSettings.shared.hiddenChatListFilterIds = hiddenIds + updateState { state in + var state = state + state.sgHiddenFilterIdsRevision += 1 + return state + } }, updateDisplayTags: { value in context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value) }, updateDisplayTagsLocked: { @@ -717,7 +744,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ChatListFilterPresetListEntry]) -> Signal in let fromEntry = entries[fromIndex] - guard case let .preset(_, _, _, fromPreset, _, _, _, _, _, _) = fromEntry else { + guard case let .preset(_, _, _, fromPreset, _, _, _, _, _, _, _) = fromEntry else { return .single(false) } var referenceFilter: ChatListFilter? @@ -725,7 +752,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch var afterAll = false if toIndex < entries.count { switch entries[toIndex] { - case let .preset(_, _, _, preset, _, _, _, _, _, _): + case let .preset(_, _, _, preset, _, _, _, _, _, _, _): referenceFilter = preset default: if entries[toIndex] < fromEntry { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 37cba290b47..32afa47c5fc 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -9,6 +9,7 @@ import ItemListUI import TelegramUIPreferences import AccountContext import TextNodeWithEntities +import SGStrings struct ChatListFilterPresetListItemEditing: Equatable { let editable: Bool @@ -29,10 +30,12 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem, ItemListRe let canBeDeleted: Bool let isAllChats: Bool let isDisabled: Bool + let isHidden: Bool let sectionId: ItemListSectionId let action: () -> Void let setItemWithRevealedOptions: (Int32?, Int32?) -> Void let remove: () -> Void + let toggleHidden: () -> Void var hasActiveRevealOptions: Bool { return self.editing.revealed @@ -51,10 +54,12 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem, ItemListRe canBeDeleted: Bool, isAllChats: Bool, isDisabled: Bool, + isHidden: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, - remove: @escaping () -> Void + remove: @escaping () -> Void, + toggleHidden: @escaping () -> Void ) { self.context = context self.presentationData = presentationData @@ -68,10 +73,12 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem, ItemListRe self.canBeDeleted = canBeDeleted self.isAllChats = isAllChats self.isDisabled = isDisabled + self.isHidden = isHidden self.sectionId = sectionId self.action = action self.setItemWithRevealedOptions = setItemWithRevealedOptions self.remove = remove + self.toggleHidden = toggleHidden } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -245,11 +252,13 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/SharedFolderListIcon"), color: item.presentationData.theme.list.disclosureArrowColor) } - let peerRevealOptions: [ItemListRevealOption] + // MARK: Swiftgram + var peerRevealOptions: [ItemListRevealOption] = [] + if item.editing.editable && !item.isAllChats { + peerRevealOptions.append(ItemListRevealOption(key: 1, title: item.isHidden ? "Settings.Folders.Show".i18n(item.presentationData.strings.baseLanguageCode) : "Settings.Folders.Hide".i18n(item.presentationData.strings.baseLanguageCode), icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.neutral1.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)) + } if item.editing.editable && item.canBeDeleted { - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)] - } else { - peerRevealOptions = [] + peerRevealOptions.append(ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)) } let titleAttributedString = NSMutableAttributedString() @@ -604,9 +613,14 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { self.setRevealOptionsOpened(false, animated: true) self.revealOptionsInteractivelyClosed() - + if let item = self.item { - item.remove() + // MARK: Swiftgram + if option.key == 1 { + item.toggleHidden() + } else { + item.remove() + } } }