diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index d3d5e444c4..f01637a93e 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -91,6 +91,7 @@ swift_library( "//submodules/InviteLinksUI", "//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen", "//submodules/ItemListUI", + "//submodules/QrCodeUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index c1ea7b5ad8..29f9b3d515 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -3217,7 +3217,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text = strongSelf.presentationData.strings.ChatList_DeletedThreads(Int32(threadIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: text), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -3301,7 +3301,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: text), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -3646,7 +3646,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return true }) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: strongSelf.presentationData.strings.Undo_ChatCleared), elevatedLayout: false, animateInAsReplacement: true, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: strongSelf.presentationData.strings.Undo_ChatCleared, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { return false } @@ -3868,7 +3868,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let statusText = self.presentationData.strings.Undo_DeletedTopic - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let self else { return false } @@ -4156,7 +4156,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return true }) - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let strongSelf = self else { return false } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index adea419845..e05fb5de5b 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -15,6 +15,10 @@ import AvatarNode import ChatListFilterSettingsHeaderItem import PremiumUI import InviteLinksUI +import QrCodeUI +import ContextUI +import AsyncDisplayKit +import UndoUI private enum FilterSection: Int32, Hashable { case include @@ -37,6 +41,7 @@ private final class ChatListFilterPresetControllerArguments { let createLink: () -> Void let openLink: (ExportedChatFolderLink) -> Void let removeLink: (ExportedChatFolderLink) -> Void + let linkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void init( context: AccountContext, @@ -53,7 +58,8 @@ private final class ChatListFilterPresetControllerArguments { expandSection: @escaping (FilterSection) -> Void, createLink: @escaping () -> Void, openLink: @escaping (ExportedChatFolderLink) -> Void, - removeLink: @escaping (ExportedChatFolderLink) -> Void + removeLink: @escaping (ExportedChatFolderLink) -> Void, + linkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void ) { self.context = context self.updateState = updateState @@ -70,6 +76,7 @@ private final class ChatListFilterPresetControllerArguments { self.createLink = createLink self.openLink = openLink self.removeLink = removeLink + self.linkContextAction = linkContextAction } } @@ -537,7 +544,9 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { arguments.openLink(invite) }, removeAction: { invite in arguments.removeLink(invite) - }, contextAction: nil) + }, contextAction: { link, node, gesture in + arguments.linkContextAction(link, node, gesture) + }) case let .inviteLinkInfo(text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) } @@ -624,7 +633,8 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio if let currentPreset, let data = currentPreset.data, data.isShared { } else { entries.append(.excludePeersHeader(presentationData.strings.ChatListFolder_ExcludedSectionHeader)) - entries.append(.addExcludePeer(title: presentationData.strings.ChatListFolder_AddChats)) + //TODO:localize + entries.append(.addExcludePeer(title: "Add Chats to Exclude")) var excludeCategoryIndex = 0 for category in ChatListFilterExcludeCategory.allCases { @@ -1085,6 +1095,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat var focusOnNameImpl: (() -> Void)? var clearFocusImpl: (() -> Void)? var applyImpl: ((Bool, @escaping () -> Void) -> Void)? + var getControllerImpl: (() -> ViewController?)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil) if let currentPreset { @@ -1375,9 +1387,10 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat if let updatedLink { if let index = links.firstIndex(where: { $0.link == link.link }) { - links.remove(at: index) + links[index] = updatedLink + } else { + links.insert(updatedLink, at: 0) } - links.insert(updatedLink, at: 0) sharedLinks.set(.single(links)) } else { if let index = links.firstIndex(where: { $0.link == link.link }) { @@ -1387,6 +1400,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } }) } + }, presentController: { c in + presentControllerImpl?(c, nil) })) }) } @@ -1404,6 +1419,59 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat actionsDisposable.add(context.engine.peers.deleteChatFolderLink(filterId: currentPreset.id, link: link).start()) }) } + }, + linkContextAction: { invite, node, gesture in + guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?(), let invite = invite, let currentPreset else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + + //dismissTooltipsImpl?() + + UIPasteboard.general.string = invite.link + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: nil, subject: .chatFolder(slug: invite.slug)), nil) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { _, f in + f(.dismissWithoutContent) + + let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in + var links = links ?? [] + if let index = links.firstIndex(where: { $0.link == invite.link }) { + links.remove(at: index) + } + sharedLinks.set(.single(links)) + }) + + let _ = (context.engine.peers.editChatFolderLink(filterId: currentPreset.id, link: invite, title: nil, peerIds: nil, revoke: true) + |> deliverOnMainQueue).start(completed: { + let _ = (context.engine.peers.deleteChatFolderLink(filterId: currentPreset.id, link: invite) + |> deliverOnMainQueue).start(completed: { + }) + }) + }))) + + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + presentInGlobalOverlayImpl?(contextController) } ) @@ -1577,6 +1645,14 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat }) })]), nil) } + getControllerImpl = { [weak controller] in + return controller + } + presentInGlobalOverlayImpl = { [weak controller] c in + if let controller = controller { + controller.presentInGlobalOverlay(c) + } + } attemptNavigationImpl = { let state = stateValue.with { $0 } if let currentPreset = currentPreset, case let .filter(currentId, currentTitle, currentEmoticon, currentData) = currentPreset { @@ -1652,7 +1728,9 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec |> deliverOnMainQueue).start(next: { existingLink in if let existingLink { completed() - pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: peerIds, currentInvitation: existingLink, linkUpdated: linkUpdated)) + pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: peerIds, currentInvitation: existingLink, linkUpdated: linkUpdated, presentController: { c in + presentController(c) + })) return } @@ -1672,7 +1750,9 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec }) if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) { completed() - pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: nil, linkUpdated: linkUpdated)) + pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: nil, linkUpdated: linkUpdated, presentController: { c in + presentController(c) + })) } else { var enabledPeerIds: [EnginePeer.Id] = [] for peer in peers { @@ -1686,7 +1766,9 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec completed() linkUpdated(link) - pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: linkUpdated)) + pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: linkUpdated, presentController: { c in + presentController(c) + })) }, error: { error in completed() //TODO:localize @@ -1714,3 +1796,17 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec }) }) } + +private final class InviteLinkContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceNode: ContextReferenceContentNode + + init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index f7c0dc6cab..2acde4d64a 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -316,7 +316,7 @@ private struct FolderInviteLinkListControllerState: Equatable { var isSaving: Bool = false } -public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filterId: Int32, title filterTitle: String, allPeerIds: [PeerId], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) -> ViewController { +public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filterId: Int32, title filterTitle: String, allPeerIds: [PeerId], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void, presentController parentPresentController: ((ViewController) -> Void)?) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? let _ = pushControllerImpl var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? @@ -346,7 +346,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese var getControllerImpl: (() -> ViewController?)? - var displayTooltipImpl: ((UndoOverlayContent) -> Void)? + var displayTooltipImpl: ((UndoOverlayContent, Bool) -> Void)? var didDisplayAddPeerNotice: Bool = false @@ -439,11 +439,6 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } } }) - /*promptController.dismissed = { byOutsideTap in - if byOutsideTap { - completionHandler(nil) - } - }*/ presentControllerImpl?(promptController, nil) }))) @@ -509,7 +504,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese dismissTooltipsImpl?() //TODO:localize - displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats.", timeout: 8)) + displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats.", timeout: 8), true) } } else { //TODO:localize @@ -532,7 +527,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } } dismissTooltipsImpl?() - displayTooltipImpl?(.peers(context: context, peers: [peer], title: nil, text: text, customUndoText: nil)) + displayTooltipImpl?(.peers(context: context, peers: [peer], title: nil, text: text, customUndoText: nil), true) } }, toggleAllSelected: { let _ = (context.engine.data.get( @@ -605,6 +600,9 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "An error occurred.", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) }, completed: { linkUpdated(ExportedChatFolderLink(title: state.title ?? "", link: currentLink.link, peerIds: Array(state.selectedPeerIds), isRevoked: false)) + //TODO:localize + displayTooltipImpl?(.info(title: nil, text: "Link updated", timeout: 3), false) + dismissImpl?() })) } else { @@ -613,7 +611,6 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } else { dismissImpl?() } - dismissImpl?() } let _ = (allPeers @@ -768,18 +765,15 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese getControllerImpl = { [weak controller] in return controller } - displayTooltipImpl = { [weak controller] c in - if let controller = controller { - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + displayTooltipImpl = { [weak controller] c, inCurrentContext in + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + if let controller = controller, inCurrentContext { controller.present(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false }), in: .current) + } else if !inCurrentContext { + parentPresentController?(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false })) } } dismissTooltipsImpl = { [weak controller] in - controller?.window?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - }) controller?.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 3537ef64cd..4cabd0bc59 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -956,26 +956,26 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio } -final class InviteLinkContextExtractedContentSource: ContextExtractedContentSource { - var keepInPlace: Bool - let ignoreContentTouches: Bool = true - let blurBackground: Bool +public final class InviteLinkContextExtractedContentSource: ContextExtractedContentSource { + public var keepInPlace: Bool + public let ignoreContentTouches: Bool = true + public let blurBackground: Bool private let controller: ViewController private let sourceNode: ContextExtractedContentContainingNode - init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool) { + public init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool) { self.controller = controller self.sourceNode = sourceNode self.keepInPlace = keepInPlace self.blurBackground = blurBackground } - func takeView() -> ContextControllerTakeViewInfo? { + public func takeView() -> ContextControllerTakeViewInfo? { return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) } - func putBack() -> ContextControllerPutBackViewInfo? { + public func putBack() -> ContextControllerPutBackViewInfo? { return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) } } diff --git a/submodules/Postbox/Sources/ChatListView.swift b/submodules/Postbox/Sources/ChatListView.swift index 4c7ee4a4ae..f6e82fa722 100644 --- a/submodules/Postbox/Sources/ChatListView.swift +++ b/submodules/Postbox/Sources/ChatListView.swift @@ -404,6 +404,7 @@ public struct ChatListViewReadState: Equatable { final class MutableChatListView { let groupId: PeerGroupId let filterPredicate: ChatListFilterPredicate? + private let aroundIndex: ChatListIndex private let summaryComponents: ChatListEntrySummaryComponents fileprivate var groupEntries: [ChatListGroupReferenceEntry] private var count: Int @@ -416,11 +417,16 @@ final class MutableChatListView { private var additionalItems: [AdditionalChatListItem] = [] fileprivate var additionalItemEntries: [MutableChatListAdditionalItemEntry] = [] + private var currentHiddenPeerIds = Set() + init(postbox: PostboxImpl, currentTransaction: Transaction, groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate?, aroundIndex: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents) { self.groupId = groupId self.filterPredicate = filterPredicate + self.aroundIndex = aroundIndex self.summaryComponents = summaryComponents + self.currentHiddenPeerIds = postbox.hiddenChatIds + var spaces: [ChatListViewSpace] = [ .group(groupId: self.groupId, pinned: .notPinned, predicate: filterPredicate) ] @@ -474,7 +480,7 @@ final class MutableChatListView { if let entry = postbox.chatListTable.earlierEntryInfos(groupId: groupId, index: upperBound, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: 1).first { switch entry { case let .message(index, messageIndex): - if let messageIndex = messageIndex { + if let messageIndex = messageIndex, !postbox.isChatHidden(peerId: messageIndex.id.peerId) { foundIndices.append((index, messageIndex)) if index.pinningIndex == nil { unpinnedCount += 1 @@ -541,7 +547,7 @@ final class MutableChatListView { func refreshDueToExternalTransaction(postbox: PostboxImpl, currentTransaction: Transaction) -> Bool { var updated = false - self.state = ChatListViewState(postbox: postbox, currentTransaction: currentTransaction, spaces: self.spaces, anchorIndex: .absoluteUpperBound, summaryComponents: self.summaryComponents, halfLimit: self.count) + self.state = ChatListViewState(postbox: postbox, currentTransaction: currentTransaction, spaces: self.spaces, anchorIndex: self.aroundIndex, summaryComponents: self.summaryComponents, halfLimit: self.count) self.sampledState = self.state.sample(postbox: postbox, currentTransaction: currentTransaction) updated = true @@ -559,8 +565,19 @@ final class MutableChatListView { func replay(postbox: PostboxImpl, currentTransaction: Transaction, operations: [PeerGroupId: [ChatListOperation]], updatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence], transaction: PostboxTransaction, context: MutableChatListViewReplayContext) -> Bool { var hasChanges = false + let hiddenChatIds = postbox.hiddenChatIds + var hasFilterChanges = false + if hiddenChatIds != self.currentHiddenPeerIds { + self.currentHiddenPeerIds = hiddenChatIds + hasFilterChanges = true + } + if transaction.updatedGlobalNotificationSettings && self.filterPredicate != nil { - self.state = ChatListViewState(postbox: postbox, currentTransaction: currentTransaction, spaces: self.spaces, anchorIndex: .absoluteUpperBound, summaryComponents: self.summaryComponents, halfLimit: self.count) + self.state = ChatListViewState(postbox: postbox, currentTransaction: currentTransaction, spaces: self.spaces, anchorIndex: self.aroundIndex, summaryComponents: self.summaryComponents, halfLimit: self.count) + self.sampledState = self.state.sample(postbox: postbox, currentTransaction: currentTransaction) + hasChanges = true + } else if hasFilterChanges { + self.state = ChatListViewState(postbox: postbox, currentTransaction: currentTransaction, spaces: self.spaces, anchorIndex: self.aroundIndex, summaryComponents: self.summaryComponents, halfLimit: self.count) self.sampledState = self.state.sample(postbox: postbox, currentTransaction: currentTransaction) hasChanges = true } else { @@ -577,6 +594,9 @@ final class MutableChatListView { invalidatedGroups = true } } + if hasFilterChanges { + invalidatedGroups = true + } if invalidatedGroups { self.reloadGroups(postbox: postbox) diff --git a/submodules/Postbox/Sources/ChatListViewState.swift b/submodules/Postbox/Sources/ChatListViewState.swift index 1cd9266f8e..20eca6a4a3 100644 --- a/submodules/Postbox/Sources/ChatListViewState.swift +++ b/submodules/Postbox/Sources/ChatListViewState.swift @@ -71,6 +71,9 @@ private func mappedChatListFilterPredicate(postbox: PostboxImpl, currentTransact let isRemovedFromTotalUnreadCount = resolvedIsRemovedFromTotalUnreadCount(globalSettings: globalNotificationSettings, peer: peer, peerSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId)) let messageTagSummaryResult = resolveChatListMessageTagSummaryResultCalculation(postbox: postbox, peerId: peer.id, threadId: nil, calculation: predicate.messageTagSummary) + if postbox.isChatHidden(peerId: peer.id) { + return false + } if predicate.includes(peer: peer, groupId: groupId, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: messageTagSummaryResult) { return true } else { @@ -178,6 +181,21 @@ private final class ChatListViewSpaceState { } } + let predicate: ((ChatListIntermediateEntry) -> Bool)? + if let filterPredicate = filterPredicate { + predicate = mappedChatListFilterPredicate(postbox: postbox, currentTransaction: currentTransaction, groupId: groupId, predicate: filterPredicate) + } else if postbox.hasHiddenChatIds { + predicate = { entry in + if postbox.isChatHidden(peerId: entry.index.messageIndex.id.peerId) { + return false + } else { + return true + } + } + } else { + predicate = nil + } + if case .includePinnedAsUnpinned = pinned { let unpinnedLowerBound: MutableChatListEntryIndex let unpinnedUpperBound: MutableChatListEntryIndex @@ -186,7 +204,7 @@ private final class ChatListViewSpaceState { let resolvedUnpinnedAnchorIndex = min(unpinnedUpperBound, max(self.anchorIndex, unpinnedLowerBound)) if lowerOrAtAnchorMessages.count < self.halfLimit || higherThanAnchorMessages.count < self.halfLimit { - let loadedMessages = postbox.chatListTable.entries(groupId: groupId, from: (ChatListIndex.pinnedLowerBound, true), to: (ChatListIndex.absoluteUpperBound, true), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: self.halfLimit * 2, predicate: filterPredicate.flatMap { mappedChatListFilterPredicate(postbox: postbox, currentTransaction: currentTransaction, groupId: groupId, predicate: $0) }).map(mapEntry).sorted(by: { $0.entryIndex < $1.entryIndex }) + let loadedMessages = postbox.chatListTable.entries(groupId: groupId, from: (ChatListIndex.pinnedLowerBound, true), to: (ChatListIndex.absoluteUpperBound, true), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: self.halfLimit * 2, predicate: predicate).map(mapEntry).sorted(by: { $0.entryIndex < $1.entryIndex }) if lowerOrAtAnchorMessages.count < self.halfLimit { var nextLowerIndex: MutableChatListEntryIndex @@ -225,7 +243,7 @@ private final class ChatListViewSpaceState { } else { nextLowerIndex = resolvedAnchorIndex.successor } - let loadedLowerMessages = postbox.chatListTable.entries(groupId: groupId, from: (nextLowerIndex.index, nextLowerIndex.isMessage), to: (lowerBound.index, lowerBound.isMessage), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: self.halfLimit - lowerOrAtAnchorMessages.count, predicate: filterPredicate.flatMap { mappedChatListFilterPredicate(postbox: postbox, currentTransaction: currentTransaction, groupId: groupId, predicate: $0) }).map(mapEntry) + let loadedLowerMessages = postbox.chatListTable.entries(groupId: groupId, from: (nextLowerIndex.index, nextLowerIndex.isMessage), to: (lowerBound.index, lowerBound.isMessage), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: self.halfLimit - lowerOrAtAnchorMessages.count, predicate: predicate).map(mapEntry) lowerOrAtAnchorMessages.append(contentsOf: loadedLowerMessages) } if higherThanAnchorMessages.count < self.halfLimit { @@ -235,7 +253,7 @@ private final class ChatListViewSpaceState { } else { nextHigherIndex = resolvedAnchorIndex } - let loadedHigherMessages = postbox.chatListTable.entries(groupId: groupId, from: (nextHigherIndex.index, nextHigherIndex.isMessage), to: (upperBound.index, upperBound.isMessage), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: self.halfLimit - higherThanAnchorMessages.count, predicate: filterPredicate.flatMap { mappedChatListFilterPredicate(postbox: postbox, currentTransaction: currentTransaction, groupId: groupId, predicate: $0) }).map(mapEntry) + let loadedHigherMessages = postbox.chatListTable.entries(groupId: groupId, from: (nextHigherIndex.index, nextHigherIndex.isMessage), to: (upperBound.index, upperBound.isMessage), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: self.halfLimit - higherThanAnchorMessages.count, predicate: predicate).map(mapEntry) higherThanAnchorMessages.append(contentsOf: loadedHigherMessages) } } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index fe1763a19d..2d35853aaa 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1236,6 +1236,16 @@ public final class Transaction { assert(!self.disposed) return self.postbox!.messageHistoryThreadPinnedTable.get(peerId: peerId) } + + func addChatHidden(peerId: PeerId) -> Int { + assert(!self.disposed) + return self.postbox!.addChatHidden(peerId: peerId) + } + + func removeChatHidden(peerId: PeerId, index: Int) { + assert(!self.disposed) + return self.postbox!.removeChatHidden(peerId: peerId, index: index) + } } public enum PostboxResult { @@ -1470,6 +1480,40 @@ final class PostboxImpl { private var currentUpdatedPeerThreadCombinedStates = Set() private var currentUpdatedPinnedThreads = Set() + private var currentHiddenChatIds: [PeerId: Bag] = [:] + private var currentUpdatedHiddenPeerIds: Bool = false + + var hiddenChatIds: Set { + if self.currentHiddenChatIds.isEmpty { + return Set() + } else { + var result = Set() + for (peerId, bag) in self.currentHiddenChatIds { + if !bag.isEmpty { + result.insert(peerId) + } + } + return result + } + } + + func isChatHidden(peerId: PeerId) -> Bool { + if let bag = self.currentHiddenChatIds[peerId], !bag.isEmpty { + return true + } else { + return false + } + } + + var hasHiddenChatIds: Bool { + for (_, bag) in self.currentHiddenChatIds { + if !bag.isEmpty { + return true + } + } + return false + } + private var currentNeedsReindexUnreadCounters: Bool = false private let statePipe: ValuePipe = ValuePipe() @@ -2117,7 +2161,7 @@ final class PostboxImpl { let updatedPeerTimeoutAttributes = self.peerTimeoutPropertiesTable.hasUpdates - let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters, updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes, updatedMessageThreadPeerIds: updatedMessageThreadPeerIds, updatedPeerThreadCombinedStates: self.currentUpdatedPeerThreadCombinedStates, updatedPeerThreadsSummaries: Set(alteredInitialPeerThreadsSummaries.keys), updatedPinnedThreads: self.currentUpdatedPinnedThreads) + let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters, updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes, updatedMessageThreadPeerIds: updatedMessageThreadPeerIds, updatedPeerThreadCombinedStates: self.currentUpdatedPeerThreadCombinedStates, updatedPeerThreadsSummaries: Set(alteredInitialPeerThreadsSummaries.keys), updatedPinnedThreads: self.currentUpdatedPinnedThreads, updatedHiddenPeerIds: self.currentUpdatedHiddenPeerIds) var updatedTransactionState: Int64? var updatedMasterClientId: Int64? if !transaction.isEmpty { @@ -2171,6 +2215,7 @@ final class PostboxImpl { self.currentUpdatedPendingPeerNotificationSettings.removeAll() self.currentGroupIdsWithUpdatedReadStats.removeAll() self.currentUpdatedPinnedThreads.removeAll() + self.currentUpdatedHiddenPeerIds = false self.currentNeedsReindexUnreadCounters = false for table in self.tables { @@ -3623,6 +3668,36 @@ final class PostboxImpl { return disposable } + public func addHiddenChatFilterAndChatIds(peerIds: [PeerId]) -> Disposable { + let disposable = MetaDisposable() + + let queue = self.queue + let _ = (self.transaction { transaction -> [PeerId: Int] in + var peerIndices: [PeerId: Int] = [:] + for peerId in peerIds { + peerIndices[peerId] = transaction.addChatHidden(peerId: peerId) + } + return peerIndices + }).start(next: { peerIndices in + disposable.set(ActionDisposable { [weak self] in + queue.async { + guard let `self` = self else { + return + } + let _ = (self.transaction { transaction -> Void in + for peerId in peerIds { + if let index = peerIndices[peerId] { + transaction.removeChatHidden(peerId: peerId, index: index) + } + } + }).start() + } + }) + }) + + return disposable + } + fileprivate func scanMessages(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags, _ f: (Message) -> Bool) { var index = MessageIndex.lowerBound(peerId: peerId, namespace: namespace) while true { @@ -3812,6 +3887,25 @@ final class PostboxImpl { self.messageHistoryThreadPinnedTable.set(peerId: peerId, threadIds: threadIds) } + fileprivate func addChatHidden(peerId: PeerId) -> Int { + let bag: Bag + if let current = self.currentHiddenChatIds[peerId] { + bag = current + } else { + bag = Bag() + self.currentHiddenChatIds[peerId] = bag + } + self.currentUpdatedHiddenPeerIds = true + return bag.add(Void()) + } + + fileprivate func removeChatHidden(peerId: PeerId, index: Int) { + if let current = self.currentHiddenChatIds[peerId] { + current.remove(index) + self.currentUpdatedHiddenPeerIds = true + } + } + fileprivate func reindexUnreadCounters(currentTransaction: Transaction) { self.groupMessageStatsTable.removeAll() let _ = CFAbsoluteTimeGetCurrent() @@ -4414,6 +4508,16 @@ public class Postbox { return disposable } + + public func addHiddenChatIds(peerIds: [PeerId]) -> Disposable { + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.addHiddenChatFilterAndChatIds(peerIds: peerIds)) + } + + return disposable + } public func isMasterClient() -> Signal { return Signal { subscriber in diff --git a/submodules/Postbox/Sources/PostboxTransaction.swift b/submodules/Postbox/Sources/PostboxTransaction.swift index 0b95cede50..118532e4dd 100644 --- a/submodules/Postbox/Sources/PostboxTransaction.swift +++ b/submodules/Postbox/Sources/PostboxTransaction.swift @@ -48,6 +48,7 @@ final class PostboxTransaction { let updatedPeerThreadCombinedStates: Set let updatedPeerThreadsSummaries: Set let updatedPinnedThreads: Set + let updatedHiddenPeerIds: Bool var isEmpty: Bool { if currentUpdatedState != nil { @@ -191,10 +192,13 @@ final class PostboxTransaction { if !self.updatedPinnedThreads.isEmpty { return false } + if self.updatedHiddenPeerIds { + return false + } return true } - init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: Set, currentUpdatedTotalUnreadStates: [PeerGroupId: ChatListTotalUnreadState], currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set, updatedCacheEntryKeys: Set, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set, updatedFailedMessageIds: Set, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool, updatedMessageThreadPeerIds: Set, updatedPeerThreadCombinedStates: Set, updatedPeerThreadsSummaries: Set, updatedPinnedThreads: Set) { + init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: Set, currentUpdatedTotalUnreadStates: [PeerGroupId: ChatListTotalUnreadState], currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, replacedAdditionalChatListItems: [AdditionalChatListItem]?, updatedNoticeEntryKeys: Set, updatedCacheEntryKeys: Set, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set, updatedFailedMessageIds: Set, updatedGlobalNotificationSettings: Bool, updatedPeerTimeoutAttributes: Bool, updatedMessageThreadPeerIds: Set, updatedPeerThreadCombinedStates: Set, updatedPeerThreadsSummaries: Set, updatedPinnedThreads: Set, updatedHiddenPeerIds: Bool) { self.currentUpdatedState = currentUpdatedState self.currentPeerHoleOperations = currentPeerHoleOperations self.currentOperationsByPeerId = currentOperationsByPeerId @@ -241,5 +245,6 @@ final class PostboxTransaction { self.updatedPeerThreadCombinedStates = updatedPeerThreadCombinedStates self.updatedPeerThreadsSummaries = updatedPeerThreadsSummaries self.updatedPinnedThreads = updatedPinnedThreads + self.updatedHiddenPeerIds = updatedHiddenPeerIds } } diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 653f386bcd..137ad7238c 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -333,6 +333,12 @@ public final class AccountViewTracker { public let chatListPreloadItems = Promise>([]) + private let hiddenChatListFilterIdsValue = Atomic<[Int32: Bag]>(value: [:]) + private let hiddenChatListFilterIdsPromise = ValuePromise>(Set()) + public var hiddenChatListFilterIds: Signal, NoError> { + return self.hiddenChatListFilterIdsPromise.get() + } + var resetPeerHoleManagement: ((PeerId) -> Void)? init(account: Account) { @@ -2162,4 +2168,52 @@ public final class AccountViewTracker { return .never() } } + + public func addHiddenChatListFilterIds(_ ids: [Int32]) -> Disposable { + var indices: [Int32: Int] = [:] + var updatedIds = Set() + let _ = self.hiddenChatListFilterIdsValue.modify { value in + var value = value + for id in ids { + let bag: Bag + if let current = value[id] { + bag = current + } else { + bag = Bag() + value[id] = bag + } + indices[id] = bag.add(Void()) + } + for (id, bag) in value { + if !bag.isEmpty { + updatedIds.insert(id) + } + } + return value + } + self.hiddenChatListFilterIdsPromise.set(updatedIds) + + return ActionDisposable { [weak self] in + DispatchQueue.main.async { + guard let `self` = self else { + return + } + var updatedIds = Set() + let _ = self.hiddenChatListFilterIdsValue.modify { value in + for id in ids { + if let bag = value[id], let index = indices[id] { + bag.remove(index) + } + } + for (id, bag) in value { + if !bag.isEmpty { + updatedIds.insert(id) + } + } + return value + } + self.hiddenChatListFilterIdsPromise.set(updatedIds) + } + } + } } diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index cb77e07285..af33514b6a 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -107,7 +107,7 @@ extension UserLimitsConfiguration { self.maxAboutLength = getValue("about_length_limit", orElse: defaultValue.maxAboutLength) self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText) self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1) - self.maxSharedFolderInviteLinks = getValue("community_invites_limit", orElse: 3) - self.maxSharedFolderJoin = getValue("communities_joined_limit", orElse: 2) + self.maxSharedFolderInviteLinks = getValue("chatlists_invites_limit", orElse: isPremium ? 100 : 3) + self.maxSharedFolderJoin = getValue("chatlists_joined_limit", orElse: isPremium ? 100 : 2) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index d94096dee1..c6acb96500 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -1000,11 +1000,20 @@ func _internal_updateChatListFiltersInteractively(transaction: Transaction, _ f: } } -func _internal_updatedChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], NoError> { - return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) - |> map { preferences -> [ChatListFilter] in +func _internal_updatedChatListFilters(postbox: Postbox, hiddenIds: Signal, NoError> = .single(Set())) -> Signal<[ChatListFilter], NoError> { + return combineLatest( + postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]), + hiddenIds + ) + |> map { preferences, hiddenIds -> [ChatListFilter] in let filtersState = preferences.values[PreferencesKeys.chatListFilters]?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default - return filtersState.filters + return filtersState.filters.filter { filter in + if hiddenIds.contains(filter.id) { + return false + } else { + return true + } + } } |> distinctUntilChanged } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 0b9c8f9286..8550402677 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -510,7 +510,7 @@ public extension TelegramEngine { } public func updatedChatListFilters() -> Signal<[ChatListFilter], NoError> { - return _internal_updatedChatListFilters(postbox: self.account.postbox) + return _internal_updatedChatListFilters(postbox: self.account.postbox, hiddenIds: self.account.viewTracker.hiddenChatListFilterIds) } public func updatedChatListFiltersInfo() -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> { diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 26577c7e7b..e1af2e0907 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -437,7 +437,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let text: String if let linkContents = component.linkContents { if case .remove = component.subject { - text = "Do you want to quit the chats you joined when\nadding the folder \(linkContents.title ?? "Folder")?" + text = "Do you also want to quit the chats included in this folder?" } else if allChatsAdded { text = "You have already added this\nfolder and its chats." } else if linkContents.localFilterId == nil { @@ -763,7 +763,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil, displaysProgress: self.inProgress, action: { [weak self] in - guard let self, let component = self.component, let controller = self.environment?.controller() else { + guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { return } @@ -773,13 +773,78 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { component.completion?() - self.joinDisposable = (component.context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(self.selectedItems)) + let disposable = DisposableSet() + disposable.add(component.context.account.postbox.addHiddenChatIds(peerIds: Array(self.selectedItems))) + disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId])) + + let folderTitle = linkContents.title ?? "" + + var additionalText: String? + if !self.selectedItems.isEmpty { + if self.selectedItems.count == 1 { + additionalText = "You also left **1** chat" + } else { + additionalText = "You also left **\(self.selectedItems.count)** chats" + } + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + var chatListController: ChatListController? + if let navigationController = controller.navigationController as? NavigationController { + for viewController in navigationController.viewControllers { + if let rootController = viewController as? TabBarController { + for c in rootController.controllers { + if let c = c as? ChatListController { + chatListController = c + break + } + } + } else if let c = viewController as? ChatListController { + chatListController = c + break + } + } + } + + let context = component.context + let selectedItems = self.selectedItems + let undoOverlayController = UndoOverlayController( + presentationData: presentationData, + content: .removedChat(title: "Folder \(folderTitle) deleted", text: additionalText), + elevatedLayout: false, + action: { value in + if case .commit = value { + let _ = (context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(selectedItems)) + |> deliverOnMainQueue).start(completed: { + Queue.mainQueue().after(1.0, { + disposable.dispose() + }) + }) + return true + } else if case .undo = value { + disposable.dispose() + return true + } + return false + } + ) + + if let chatListController, chatListController.view.window != nil { + chatListController.present(undoOverlayController, in: .current) + } else { + controller.present(undoOverlayController, in: .window(.root)) + } + + controller.dismiss() + + /*self.joinDisposable = (component.context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(self.selectedItems)) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self, let controller = self.environment?.controller() else { return } controller.dismiss() - }) + })*/ } else if allChatsAdded { controller.dismiss() } else if let _ = component.linkContents { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f11e30c388..15c86331c0 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -11990,7 +11990,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G statusText = self.presentationData.strings.Undo_ChatCleared } - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, action: { [weak self] value in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, action: { [weak self] value in guard let strongSelf = self else { return false } @@ -15911,7 +15911,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = range - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, action: { value in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, action: { value in guard let strongSelf = self else { return false } diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 6ab2793063..84788257c3 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -7,7 +7,7 @@ import AccountContext import ComponentFlow public enum UndoOverlayContent { - case removedChat(text: String) + case removedChat(title: String, text: String?) case archivedChat(peerId: Int64, title: String, text: String, undo: Bool) case hidArchive(title: String, text: String, undo: Bool) case revealedArchive(title: String, text: String, undo: Bool) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 2fe7ddeddd..94bdb4ae57 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -94,13 +94,22 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { var isUserInteractionEnabled = false switch content { - case let .removedChat(text): + case let .removedChat(title, text): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil self.animationNode = nil self.animatedStickerNode = nil - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) + if let text { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) + self.textNode.attributedText = attributedText + } else { + self.textNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } displayUndo = true self.originalRemainingSeconds = 5 self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)