diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 1011ebec6f..3e07d10605 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -120,6 +120,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, private var badgeDisposable: Disposable? private var badgeIconDisposable: Disposable? + private var didAppear = false private var dismissSearchOnDisappear = false private var passcodeLockTooltipDisposable = MetaDisposable() @@ -133,7 +134,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, private var presentationDataDisposable: Disposable? private let stateDisposable = MetaDisposable() - private var filterDisposable: Disposable? + private var filterDisposable = MetaDisposable() + + private let isReorderingTabsValue = ValuePromise(false) private var searchContentNode: NavigationBarSearchContentNode? @@ -237,7 +240,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } strongSelf.chatListDisplayNode.containerNode.currentItemNode.scrollToPosition(.top) case let .known(offset): - if offset <= navigationBarSearchContentHeight + 1.0 { + if offset <= navigationBarSearchContentHeight + 1.0 && strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter != nil { strongSelf.tabContainerNode.tabSelected?(.all) } else { if let searchContentNode = strongSelf.searchContentNode { @@ -272,8 +275,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, context.account.networkState, hasProxy, passcode, - self.chatListDisplayNode.containerNode.currentItemState - ).start(next: { [weak self] networkState, proxy, passcode, state in + self.chatListDisplayNode.containerNode.currentItemState, + self.isReorderingTabsValue.get() + ).start(next: { [weak self] networkState, proxy, passcode, state, isReorderingTabs in if let strongSelf = self { let defaultTitle: String if strongSelf.groupId == .root { @@ -292,9 +296,29 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, var isRoot = false if case .root = strongSelf.groupId { isRoot = true - let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed)) - rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose - strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem + + if isReorderingTabs { + let rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.reorderingDonePressed)) + strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem + } else { + let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed)) + rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose + strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem + } + + if isReorderingTabs { + strongSelf.navigationItem.leftBarButtonItem = nil + } else { + let editItem: UIBarButtonItem + if state.editing { + editItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(strongSelf.donePressed)) + editItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Done + } else { + editItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(strongSelf.editPressed)) + editItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Edit + } + strongSelf.navigationItem.leftBarButtonItem = editItem + } } let (hasProxy, connectsViaProxy) = proxy @@ -397,7 +421,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } } - if self.filter == nil { + if self.filter == nil, case .root = self.groupId { self.chatListDisplayNode.containerNode.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in guard let strongSelf = self else { return @@ -411,67 +435,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, if force { strongSelf.tabContainerNode.cancelAnimations() } - strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData, selectedFilter: filter, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) + strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, isEditing: false, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) } - let preferencesKey: PostboxViewKey = .preferences(keys: Set([ - ApplicationSpecificPreferencesKeys.chatListFilterSettings - ])) - let filterItems = chatListFilterItems(context: context) - self.filterDisposable = (combineLatest(queue: .mainQueue(), - context.account.postbox.combinedView(keys: [ - preferencesKey - ]), - filterItems - ) - |> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems in - guard let strongSelf = self else { - return - } - let (_, items) = countAndFilterItems - var filterItems: [ChatListFilterTabEntry] = [] - filterItems.append(.all(unreadCount: 0)) - for (filter, unreadCount) in items { - filterItems.append(.filter(id: filter.id, text: filter.title, unreadCount: unreadCount)) - } - - var resolvedItems = filterItems - if groupId != .root { - resolvedItems = [] - } - - var wasEmpty = false - if let tabContainerData = strongSelf.tabContainerData { - wasEmpty = tabContainerData.count <= 1 - } else { - wasEmpty = true - } - let selectedEntryId = strongSelf.chatListDisplayNode.containerNode.currentItemFilter - strongSelf.tabContainerData = resolvedItems - var availableFilters: [ChatListContainerNodeFilter] = [] - availableFilters.append(.all) - for item in items { - availableFilters.append(.filter(item.0)) - } - strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters) - - let isEmpty = resolvedItems.count <= 1 - - if wasEmpty != isEmpty { - strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode) - if let parentController = strongSelf.parent as? TabBarController { - parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode) - } - } - - if let layout = strongSelf.validLayout { - if wasEmpty != isEmpty { - strongSelf.containerLayoutUpdated(layout, transition: .immediate) - (strongSelf.parent as? TabBarController)?.updateLayout() - } else { - strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) - } - } - }) + self.reloadFilters() } self.tabContainerNode.tabSelected = { [weak self] id in @@ -510,6 +476,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, }) } + self.tabContainerNode.tabRequestedDeletion = { [weak self] id in + if case let .filter(id) = id { + self?.askForFilterRemoval(id: id) + } + } + self.tabContainerNode.addFilter = { [weak self] in self?.openFilterSettings() } @@ -527,7 +499,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return } var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Edit, icon: { theme in + //TODO:localization + items.append(.action(ContextMenuActionItem(text: "Edit Filter", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { @@ -597,34 +570,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, guard let strongSelf = self else { return } - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: "This will remove the filter, your chats will not be deleted."), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { - return - } - let _ = updateChatListFilterSettingsInteractively(postbox: strongSelf.context.account.postbox, { settings in - var settings = settings - settings.filters = settings.filters.filter({ $0.id != id }) - return settings - }).start() - let _ = replaceRemoteChatListFilters(account: strongSelf.context.account).start() - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - strongSelf.present(actionSheet, in: .window(.root)) + strongSelf.askForFilterRemoval(id: id) }) }))) + + if filters.count > 1 { + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: "Reorder Tabs", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) + }, action: { c, f in + //f(.default) + c.dismiss(completion: { + guard let strongSelf = self else { + return + } + + strongSelf.chatListDisplayNode.isReorderingFilters = true + strongSelf.isReorderingTabsValue.set(true) + strongSelf.searchContentNode?.setIsEnabled(false, animated: true) + if let layout = strongSelf.validLayout { + strongSelf.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + }) + }))) + } } let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture) @@ -646,7 +615,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self.suggestLocalizationDisposable.dispose() self.presentationDataDisposable?.dispose() self.stateDisposable.dispose() - self.filterDisposable?.dispose() + self.filterDisposable.dispose() } private func updateThemeAndStrings() { @@ -686,7 +655,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) if let layout = self.validLayout { - self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) + self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, isEditing: false, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) } if self.isNodeLoaded { @@ -879,10 +848,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } } - self.chatListDisplayNode.dismissSelf = { [weak self] in + self.chatListDisplayNode.dismissSelfIfCompletedPresentation = { [weak self] in guard let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController else { return } + if !strongSelf.didAppear { + return + } navigationController.filterController(strongSelf, animated: true) } @@ -1025,6 +997,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + self.didAppear = true + guard case .root = self.groupId else { return } @@ -1119,7 +1093,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, if let _ = self.validLayout, let parentController = self.parent as? TabBarController, let sourceFrame = parentController.frameForControllerTab(controller: self) { let absoluteFrame = sourceFrame //TODO:localize - parentController.present(TooltipScreen(text: "Hold the Chats icon for quick access to the list of chat filters.", location: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0)), in: .current) + //parentController.present(TooltipScreen(text: "Hold the Chats icon for quick access to the list of chat filters.", location: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0)), in: .current) } }) } @@ -1174,6 +1148,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self.validLayout = layout + self.updateLayout(layout: layout, transition: transition) + + if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver { + searchContentNode.updateListVisibleContentOffset(.known(0.0)) + self.chatListDisplayNode.scrollToTop() + } + } + + private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { var tabContainerOffset: CGFloat = 0.0 if !self.displayNavigationBar { tabContainerOffset += layout.statusBarHeight ?? 0.0 @@ -1181,12 +1164,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.visualNavigationInsetHeight - self.additionalHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0))) - self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) - - if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver { - searchContentNode.updateListVisibleContentOffset(.known(0.0)) - self.chatListDisplayNode.scrollToTop() - } + self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, isEditing: false, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, visualNavigationHeight: self.visualNavigationInsetHeight, cleanNavigationBarHeight: self.cleanNavigationHeight, transition: transition) } @@ -1213,9 +1191,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, state.peerIdWithRevealedOptions = nil return state } + self.chatListDisplayNode.isEditing = true + if let layout = self.validLayout { + self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut)) + } } @objc private func donePressed() { + self.reorderingDonePressed() + let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) editItem.accessibilityLabel = self.presentationData.strings.Common_Edit if case .root = self.groupId, self.filter == nil { @@ -1232,6 +1216,173 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, state.selectedPeerIds.removeAll() return state } + self.chatListDisplayNode.isEditing = false + if let layout = self.validLayout { + self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + + @objc private func reorderingDonePressed() { + if let reorderedFilterIds = self.tabContainerNode.reorderedFilterIds { + let _ = (updateChatListFilterSettingsInteractively(postbox: self.context.account.postbox, { state in + var state = state + var updatedFilters: [ChatListFilter] = [] + for id in reorderedFilterIds { + if let index = state.filters.firstIndex(where: { $0.id == id }) { + updatedFilters.append(state.filters[index]) + } + } + updatedFilters.append(contentsOf: state.filters.compactMap { filter -> ChatListFilter? in + if !updatedFilters.contains(where: { $0.id == filter.id }) { + return filter + } else { + return nil + } + }) + state.filters = updatedFilters + return state + }) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + let _ = replaceRemoteChatListFilters(account: strongSelf.context.account).start() + strongSelf.reloadFilters(firstUpdate: { + guard let strongSelf = self else { + return + } + strongSelf.chatListDisplayNode.isReorderingFilters = false + strongSelf.isReorderingTabsValue.set(false) + strongSelf.searchContentNode?.setIsEnabled(true, animated: true) + if let layout = strongSelf.validLayout { + strongSelf.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + }) + }) + } + } + + private func reloadFilters(firstUpdate: (() -> Void)? = nil) { + let preferencesKey: PostboxViewKey = .preferences(keys: Set([ + ApplicationSpecificPreferencesKeys.chatListFilterSettings + ])) + let filterItems = chatListFilterItems(context: self.context) + var notifiedFirstUpdate = false + self.filterDisposable.set((combineLatest(queue: .mainQueue(), + context.account.postbox.combinedView(keys: [ + preferencesKey + ]), + filterItems + ) + |> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems in + guard let strongSelf = self else { + return + } + let (_, items) = countAndFilterItems + var filterItems: [ChatListFilterTabEntry] = [] + filterItems.append(.all(unreadCount: 0)) + for (filter, unreadCount) in items { + filterItems.append(.filter(id: filter.id, text: filter.title, unreadCount: unreadCount)) + } + + var resolvedItems = filterItems + if strongSelf.groupId != .root { + resolvedItems = [] + } + + var wasEmpty = false + if let tabContainerData = strongSelf.tabContainerData { + wasEmpty = tabContainerData.count <= 1 + } else { + wasEmpty = true + } + var selectedEntryId = strongSelf.chatListDisplayNode.containerNode.currentItemFilter + var resetCurrentEntry = false + if !resolvedItems.contains(where: { $0.id == selectedEntryId }) { + resetCurrentEntry = true + if let tabContainerData = strongSelf.tabContainerData { + var found = false + if let index = tabContainerData.firstIndex(where: { $0.id == selectedEntryId }) { + for i in (0 ..< index - 1).reversed() { + if resolvedItems.contains(where: { $0.id == tabContainerData[i].id }) { + selectedEntryId = tabContainerData[i].id + found = true + break + } + } + } + if !found { + selectedEntryId = .all + } + } else { + selectedEntryId = .all + } + } + strongSelf.tabContainerData = resolvedItems + var availableFilters: [ChatListContainerNodeFilter] = [] + availableFilters.append(.all) + for item in items { + availableFilters.append(.filter(item.0)) + } + strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters) + + let isEmpty = resolvedItems.count <= 1 + + if wasEmpty != isEmpty { + strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode) + if let parentController = strongSelf.parent as? TabBarController { + parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode) + } + } + + if let layout = strongSelf.validLayout { + if wasEmpty != isEmpty { + strongSelf.containerLayoutUpdated(layout, transition: .immediate) + (strongSelf.parent as? TabBarController)?.updateLayout() + } else { + strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + } + } + + if !notifiedFirstUpdate { + notifiedFirstUpdate = true + firstUpdate?() + } + + if resetCurrentEntry { + strongSelf.tabContainerNode.tabSelected?(selectedEntryId) + } + })) + } + + private func askForFilterRemoval(id: Int32) { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + + //TODO:localization + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: "This will remove the filter, your chats will not be deleted."), + ActionSheetButtonItem(title: "Remove", color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let strongSelf = self else { + return + } + let _ = updateChatListFilterSettingsInteractively(postbox: strongSelf.context.account.postbox, { settings in + var settings = settings + settings.filters = settings.filters.filter({ $0.id != id }) + return settings + }).start() + let _ = replaceRemoteChatListFilters(account: strongSelf.context.account).start() + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.present(actionSheet, in: .window(.root)) } public func activateSearch() { @@ -1457,7 +1608,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } else { let groupId = self.groupId signal = self.context.account.postbox.transaction { transaction -> Void in - markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: (self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data).flatMap(chatListFilterPredicate)) + let filterPredicate = (self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data).flatMap(chatListFilterPredicate) + markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: filterPredicate) + if let filterPredicate = filterPredicate { + for additionalGroupId in filterPredicate.includeAdditionalPeerGroupIds { + markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: additionalGroupId, filterPredicate: filterPredicate) + } + } } } let _ = (signal @@ -2155,7 +2312,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } else { if preset.data.categories == .channels { filterType = .channels - } else if preset.data.categories.isSubset(of: [.smallGroups, .largeGroups]) { + } else if preset.data.categories == .groups { filterType = .groups } else if preset.data.categories == .bots { filterType = .bots diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 12fcd234cf..446d9d5423 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -147,7 +147,9 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private(set) var transitionFraction: CGFloat = 0.0 private var transitionFractionOffset: CGFloat = 0.0 private var disableItemNodeOperationsWhileAnimating: Bool = false - private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat)? + private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, isReorderingFilters: Bool, isEditing: Bool)? + + private var panRecognizer: InteractiveTransitionGestureRecognizer? private let _ready = Promise() var ready: Signal { @@ -287,6 +289,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { panRecognizer.delegate = self panRecognizer.delaysTouchesBegan = false panRecognizer.cancelsTouchesInView = true + self.panRecognizer = panRecognizer self.view.addGestureRecognizer(panRecognizer) } @@ -312,7 +315,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { switch recognizer.state { case .began: self.transitionFractionOffset = 0.0 - if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let itemNode = self.itemNodes[self.selectedId] { + if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout, let itemNode = self.itemNodes[self.selectedId] { if let presentationLayer = itemNode.layer.presentation() { self.transitionFraction = presentationLayer.frame.minX / layout.size.width self.transitionFractionOffset = self.transitionFraction @@ -320,13 +323,13 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { for (_, itemNode) in self.itemNodes { itemNode.layer.removeAllAnimations() } - self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate) + self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate) self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, true) } } } case .changed: - if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) { + if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) { let translation = recognizer.translation(in: self.view) var transitionFraction = translation.x / layout.size.width if selectedIndex <= 0 { @@ -344,11 +347,11 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } } - self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate) + self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate) self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, false) } case .cancelled, .ended: - if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) { + if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) { let translation = recognizer.translation(in: self.view) let velocity = recognizer.velocity(in: self.view) var directionIsToRight: Bool? @@ -387,12 +390,12 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.transitionFraction = 0.0 let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) self.disableItemNodeOperationsWhileAnimating = true - self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition) + self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: transition) self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false) DispatchQueue.main.async { self.disableItemNodeOperationsWhileAnimating = false - if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout { - self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate) + if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout { + self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate) } } } @@ -449,14 +452,14 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { func updateAvailableFilters(_ availableFilters: [ChatListContainerNodeFilter]) { if self.availableFilters != availableFilters { self.availableFilters = availableFilters - if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout { - self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate) + if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout { + self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate) } } } func switchToFilter(id: ChatListFilterTabEntryId) { - guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout else { + guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout else { return } if id != self.selectedId, let index = self.availableFilters.firstIndex(where: { $0.id == id }) { @@ -467,7 +470,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { } self.applyItemNodeAsCurrent(id: id, itemNode: itemNode) let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) - self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition) + self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: transition) self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false) } else if self.pendingItemNode == nil { let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[index].filter, previewing: self.previewing, presentationData: self.presentationData, becameEmpty: { [weak self] filter in @@ -485,7 +488,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else { return } - guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = strongSelf.validLayout else { + guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = strongSelf.validLayout else { return } strongSelf.pendingItemNode = nil @@ -542,7 +545,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { } strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode) - strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate) + strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate) strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, transition, false) } @@ -551,8 +554,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } - func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) + func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, isReorderingFilters: Bool, isEditing: Bool, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight @@ -560,6 +563,11 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { insets.left += layout.safeInsets.left insets.right += layout.safeInsets.right + transition.updateAlpha(node: self, alpha: isReorderingFilters ? 0.5 : 1.0) + self.isUserInteractionEnabled = !isReorderingFilters + + self.panRecognizer?.isEnabled = !isEditing + if let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) { var validNodeIds: [ChatListFilterTabEntryId] = [] for i in max(0, selectedIndex - 1) ... min(self.availableFilters.count - 1, selectedIndex + 1) { @@ -642,6 +650,9 @@ final class ChatListControllerNode: ASDisplayNode { private(set) var searchDisplayController: SearchDisplayController? + var isReorderingFilters: Bool = false + var isEditing: Bool = false + private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat)? var requestDeactivateSearch: (() -> Void)? @@ -650,7 +661,7 @@ final class ChatListControllerNode: ASDisplayNode { var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? var requestAddContact: ((String) -> Void)? var peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)? - var dismissSelf: (() -> Void)? + var dismissSelfIfCompletedPresentation: (() -> Void)? var isEmptyUpdated: ((Bool) -> Void)? var emptyListAction: (() -> Void)? @@ -688,7 +699,7 @@ final class ChatListControllerNode: ASDisplayNode { return } if case .group = strongSelf.groupId { - strongSelf.dismissSelf?() + strongSelf.dismissSelfIfCompletedPresentation?() } } filterEmptyAction = { [weak self] filter in @@ -771,7 +782,7 @@ final class ChatListControllerNode: ASDisplayNode { } transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - self.containerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition) + self.containerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, transition: transition) if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: transition) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift index 1b2f826591..52751ffef9 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift @@ -13,8 +13,7 @@ import ChatListSearchItemHeader enum ChatListFilterCategoryIcon { case contacts case nonContacts - case smallGroups - case largeGroups + case groups case channels case bots case muted @@ -245,12 +244,9 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL case .nonContacts: color = .yellow imageName = "Chat/Context Menu/UnknownUser" - case .smallGroups: + case .groups: color = .green imageName = "Chat/Context Menu/Groups" - case .largeGroups: - color = .purple - imageName = "Chat/Context Menu/LargeGroup" case .channels: color = .red imageName = "Chat/Context Menu/Channels" diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 341b706ea6..57f3ea2d8c 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -6,6 +6,7 @@ import Postbox import TelegramCore import SyncCore import TelegramPresentationData +import PresentationDataUtils import ItemListUI import AccountContext import TelegramUIPreferences @@ -51,6 +52,7 @@ private final class ChatListFilterPresetControllerArguments { } private enum ChatListFilterPresetControllerSection: Int32 { + case screenHeader case name case includePeers case excludePeers @@ -66,14 +68,24 @@ private enum ChatListFilterPresetEntryStableId: Hashable { } private enum ChatListFilterPresetEntrySortId: Comparable { + case screenHeader case topIndex(Int) case includeIndex(Int) case excludeIndex(Int) static func <(lhs: ChatListFilterPresetEntrySortId, rhs: ChatListFilterPresetEntrySortId) -> Bool { switch lhs { + case .screenHeader: + switch rhs { + case .screenHeader: + return false + default: + return true + } case let .topIndex(lhsIndex): switch rhs { + case .screenHeader: + return false case let .topIndex(rhsIndex): return lhsIndex < rhsIndex case .includeIndex: @@ -83,6 +95,8 @@ private enum ChatListFilterPresetEntrySortId: Comparable { } case let .includeIndex(lhsIndex): switch rhs { + case .screenHeader: + return false case .topIndex: return false case let .includeIndex(rhsIndex): @@ -92,6 +106,8 @@ private enum ChatListFilterPresetEntrySortId: Comparable { } case let .excludeIndex(lhsIndex): switch rhs { + case .screenHeader: + return false case .topIndex: return false case .includeIndex: @@ -106,8 +122,7 @@ private enum ChatListFilterPresetEntrySortId: Comparable { private enum ChatListFilterIncludeCategory: Int32, CaseIterable { case contacts case nonContacts - case smallGroups - case largeGroups + case groups case channels case bots @@ -117,10 +132,8 @@ private enum ChatListFilterIncludeCategory: Int32, CaseIterable { return .contacts case .nonContacts: return .nonContacts - case .smallGroups: - return .smallGroups - case .largeGroups: - return .largeGroups + case .groups: + return .groups case .channels: return .channels case .bots: @@ -134,10 +147,8 @@ private enum ChatListFilterIncludeCategory: Int32, CaseIterable { return "Contacts" case .nonContacts: return "Non-Contacts" - case .smallGroups: - return "Small Groups" - case .largeGroups: - return "Large Groups" + case .groups: + return "Groups" case .channels: return "Channels" case .bots: @@ -170,10 +181,8 @@ private extension ChatListFilterCategoryIcon { self = .contacts case .nonContacts: self = .nonContacts - case .smallGroups: - self = .smallGroups - case .largeGroups: - self = .largeGroups + case .groups: + self = .groups case .channels: self = .channels case .bots: @@ -200,6 +209,7 @@ private enum ChatListFilterRevealedItemId: Equatable { } private enum ChatListFilterPresetEntry: ItemListNodeEntry { + case screenHeader case nameHeader(String) case name(placeholder: String, value: String) case includePeersHeader(String) @@ -215,6 +225,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { + case .screenHeader: + return ChatListFilterPresetControllerSection.screenHeader.rawValue case .nameHeader, .name: return ChatListFilterPresetControllerSection.name.rawValue case .includePeersHeader, .addIncludePeer, .includeCategory, .includePeer, .includePeerInfo: @@ -226,26 +238,28 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { var stableId: ChatListFilterPresetEntryStableId { switch self { - case .nameHeader: + case .screenHeader: return .index(0) - case .name: + case .nameHeader: return .index(1) - case .includePeersHeader: + case .name: return .index(2) - case .addIncludePeer: + case .includePeersHeader: return .index(3) + case .addIncludePeer: + return .index(4) case let .includeCategory(includeCategory): return .includeCategory(includeCategory.category) case .includePeerInfo: - return .index(4) - case .excludePeersHeader: return .index(5) - case .addExcludePeer: + case .excludePeersHeader: return .index(6) + case .addExcludePeer: + return .index(7) case let .excludeCategory(excludeCategory): return .excludeCategory(excludeCategory.category) case .excludePeerInfo: - return .index(7) + return .index(8) case let .includePeer(peer): return .peer(peer.peer.peerId) case let .excludePeer(peer): @@ -255,6 +269,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { private var sortIndex: ChatListFilterPresetEntrySortId { switch self { + case .screenHeader: + return .screenHeader case .nameHeader: return .topIndex(0) case .name: @@ -289,6 +305,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChatListFilterPresetControllerArguments switch self { + case .screenHeader: + return ChatListFilterSettingsHeaderItem(theme: presentationData.theme, text: "", animation: .newFolder, sectionId: self.section) case let .nameHeader(title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .name(placeholder, value): @@ -384,14 +402,35 @@ private struct ChatListFilterPresetControllerState: Equatable { if self.name.isEmpty { return false } + + let defaultCategories: ChatListFilterPeerCategories = .all + let defaultExcludeArchived = true + let defaultExcludeMuted = false + let defaultExcludeRead = false + + if self.includeCategories == defaultCategories && + self.excludeArchived == defaultExcludeArchived && + self.excludeMuted == defaultExcludeMuted && + self.excludeRead == defaultExcludeRead { + return false + } + + if self.includeCategories.isEmpty && self.additionallyIncludePeers.isEmpty { + return false + } + return true } } //TODO:localization -private func chatListFilterPresetControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetControllerState, includePeers: [RenderedPeer], excludePeers: [RenderedPeer]) -> [ChatListFilterPresetEntry] { +private func chatListFilterPresetControllerEntries(presentationData: PresentationData, isNewFilter: Bool, state: ChatListFilterPresetControllerState, includePeers: [RenderedPeer], excludePeers: [RenderedPeer]) -> [ChatListFilterPresetEntry] { var entries: [ChatListFilterPresetEntry] = [] + if isNewFilter { + entries.append(.screenHeader) + } + entries.append(.nameHeader("FILTER NAME")) entries.append(.name(placeholder: "Filter Name", value: state.name)) @@ -445,8 +484,7 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio private enum AdditionalCategoryId: Int { case contacts case nonContacts - case smallGroups - case largeGroups + case groups case channels case bots } @@ -474,14 +512,9 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f title: "Non-Contacts" ), ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.smallGroups.rawValue, + id: AdditionalCategoryId.groups.rawValue, icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Groups"), color: .white), color: .green), - title: "Small Groups" - ), - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.largeGroups.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/LargeGroup"), color: .white), color: .purple), - title: "Large Groups" + title: "Groups" ), ChatListNodeAdditionalCategory( id: AdditionalCategoryId.channels.rawValue, @@ -498,8 +531,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f let categoryMapping: [ChatListFilterPeerCategories: AdditionalCategoryId] = [ .contacts: .contacts, .nonContacts: .nonContacts, - .smallGroups: .smallGroups, - .largeGroups: .largeGroups, + .groups: .groups, .channels: .channels, .bots: .bots ] @@ -658,7 +690,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } else { initialName = "New Filter" } - let initialState = ChatListFilterPresetControllerState(name: initialName, includeCategories: currentPreset?.data.categories ?? .all, excludeMuted: currentPreset?.data.excludeMuted ?? false, excludeRead: currentPreset?.data.excludeRead ?? false, excludeArchived: currentPreset?.data.excludeArchived ?? false, additionallyIncludePeers: currentPreset?.data.includePeers ?? [], additionallyExcludePeers: currentPreset?.data.excludePeers ?? []) + let initialState = ChatListFilterPresetControllerState(name: initialName, includeCategories: currentPreset?.data.categories ?? [], excludeMuted: currentPreset?.data.excludeMuted ?? false, excludeRead: currentPreset?.data.excludeRead ?? false, excludeArchived: currentPreset?.data.excludeArchived ?? false, additionallyIncludePeers: currentPreset?.data.includePeers ?? [], additionallyExcludePeers: currentPreset?.data.excludePeers ?? []) let stateValue = Atomic(value: initialState) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in @@ -816,6 +848,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } } + var attemptNavigationImpl: (() -> Bool)? + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, stateWithPeers @@ -825,7 +859,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let (state, includePeers, excludePeers) = stateWithPeers let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { - dismissImpl?() + let _ = attemptNavigationImpl?() }) let rightNavigationButton = ItemListNavigationButton(content: .text(currentPreset == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: { let state = stateValue.with { $0 } @@ -845,6 +879,12 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } } if !found { + settings.filters = settings.filters.filter { listFilter in + if listFilter.title == preset.title && listFilter.data == preset.data { + return false + } + return true + } settings.filters.append(preset) } } else { @@ -861,7 +901,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat }) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(currentPreset != nil ? "Filter" : "Create Filter"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, state: state, includePeers: includePeers, excludePeers: excludePeers), style: .blocks, emptyStateItem: nil, animateChanges: !skipStateAnimation) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, isNewFilter: currentPreset == nil, state: state, includePeers: includePeers, excludePeers: excludePeers), style: .blocks, emptyStateItem: nil, animateChanges: !skipStateAnimation) skipStateAnimation = false return (controllerState, (listState, arguments)) @@ -888,6 +928,31 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } } } + controller.attemptNavigation = { _ in + return attemptNavigationImpl?() ?? true + } + let displaySaveAlert: () -> Void = { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(textAlertController(context: context, title: nil, text: "Are you sure you want to discard this folder?", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { + dismissImpl?() + })]), nil) + } + attemptNavigationImpl = { + let state = stateValue.with { $0 } + if let currentPreset = currentPreset { + let filter = ChatListFilter(id: currentPreset.id, title: state.name, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: state.additionallyIncludePeers, excludePeers: state.additionallyExcludePeers)) + if currentPreset != filter { + displaySaveAlert() + return false + } + } else { + if state.isComplete { + displaySaveAlert() + return false + } + } + return true + } return controller } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 7968e19a6b..bfe3f919f7 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -124,7 +124,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { let arguments = arguments as! ChatListFilterPresetListControllerArguments switch self { case let .screenHeader(text): - return ChatListFilterSettingsHeaderItem(theme: presentationData.theme, text: text, sectionId: self.section) + return ChatListFilterSettingsHeaderItem(theme: presentationData.theme, text: text, animation: .folders, sectionId: self.section) case let .suggestedListHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) case let .suggestedPreset(_, title, label, preset): @@ -272,7 +272,7 @@ public func chatListFilterPresetListController(context: AccountContext, updated: let suggestedFilters: [(String, String, ChatListFilterData)] = [ ("Unread", "All unread chats", ChatListFilterData(categories: .all, excludeMuted: false, excludeRead: true, excludeArchived: false, includePeers: [], excludePeers: [])), - ("Personal", "Exclude large groups and channels", ChatListFilterData(categories: ChatListFilterPeerCategories.all.subtracting([.largeGroups, .channels]), excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: [], excludePeers: [])), + ("Personal", "Exclude groups and channels", ChatListFilterData(categories: ChatListFilterPeerCategories.all.subtracting([.groups, .channels]), excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: [], excludePeers: [])), ] let signal = combineLatest(queue: .mainQueue(), diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index d839e97504..77855286cc 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -351,7 +351,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) - let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width, y: 11.0), size: labelLayout.size) + let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: 11.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0) @@ -362,7 +362,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } if let arrowImage = strongSelf.arrowNode.image { - strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width + revealOffset, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) } strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height)) @@ -432,6 +432,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } let leftInset: CGFloat = 16.0 + params.leftInset + let rightArrowInset: CGFloat = 34.0 + params.rightInset let editingOffset: CGFloat if let editableControlNode = self.editableControlNode { @@ -444,6 +445,14 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) + + var labelFrame = self.labelNode.frame + labelFrame.origin.x = params.width - rightArrowInset - labelFrame.width + revealOffset + transition.updateFrame(node: self.labelNode, frame: labelFrame) + + var arrowFrame = self.arrowNode.frame + arrowFrame.origin.x = params.width - params.rightInset - 7.0 - arrowFrame.width + revealOffset + transition.updateFrame(node: self.arrowNode, frame: arrowFrame) } override func revealOptionsInteractivelyOpened() { diff --git a/submodules/ChatListUI/Sources/ChatListFilterSettingsHeaderItem.swift b/submodules/ChatListUI/Sources/ChatListFilterSettingsHeaderItem.swift index 5a94073e49..a8a201d89e 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterSettingsHeaderItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterSettingsHeaderItem.swift @@ -9,14 +9,21 @@ import PresentationDataUtils import AnimatedStickerNode import AppBundle +enum ChatListFilterSettingsHeaderAnimation { + case folders + case newFolder +} + class ChatListFilterSettingsHeaderItem: ListViewItem, ItemListItem { let theme: PresentationTheme let text: String + let animation: ChatListFilterSettingsHeaderAnimation let sectionId: ItemListSectionId - init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId) { + init(theme: PresentationTheme, text: String, animation: ChatListFilterSettingsHeaderAnimation, sectionId: ItemListSectionId) { self.theme = theme self.text = text + self.animation = animation self.sectionId = sectionId } @@ -72,10 +79,6 @@ class ChatListFilterSettingsHeaderItemNode: ListViewItemNode { self.titleNode.contentsScale = UIScreen.main.scale self.animationNode = AnimatedStickerNode() - if let path = getAppBundle().path(forResource: "ChatListFolders", ofType: "tgs") { - self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct) - self.animationNode.visibility = true - } super.init(layerBacked: false, dynamicBounce: false) @@ -100,6 +103,20 @@ class ChatListFilterSettingsHeaderItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { + if strongSelf.item == nil { + let animationName: String + switch item.animation { + case .folders: + animationName = "ChatListFolders" + case .newFolder: + animationName = "ChatListNewFolder" + } + if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") { + strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct) + strongSelf.animationNode.visibility = true + } + } + strongSelf.item = item strongSelf.accessibilityLabel = attributedText.string diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index c53193ae9a..27209ce1a0 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -7,36 +7,94 @@ import Postbox import TelegramCore import TelegramPresentationData +private final class ItemNodeDeleteButtonNode: HighlightableButtonNode { + private let pressed: () -> Void + + private let contentImageNode: ASImageNode + + private var theme: PresentationTheme? + + init(pressed: @escaping () -> Void) { + self.pressed = pressed + + self.contentImageNode = ASImageNode() + + super.init() + + self.addSubnode(self.contentImageNode) + + self.addTarget(self, action: #selector(self.pressedEvent), forControlEvents: .touchUpInside) + } + + @objc private func pressedEvent() { + self.pressed() + } + + func update(theme: PresentationTheme) -> CGSize { + let size = CGSize(width: 18.0, height: 18.0) + if self.theme !== theme { + self.theme = theme + self.contentImageNode.image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.rootController.navigationBar.clearButtonBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.rootController.navigationBar.clearButtonForegroundColor.cgColor) + context.setLineWidth(1.5) + context.setLineCap(.round) + context.move(to: CGPoint(x: 6.38, y: 6.38)) + context.addLine(to: CGPoint(x: 11.63, y: 11.63)) + context.strokePath() + context.move(to: CGPoint(x: 6.38, y: 11.63)) + context.addLine(to: CGPoint(x: 11.63, y: 6.38)) + context.strokePath() + }) + } + + self.contentImageNode.frame = CGRect(origin: CGPoint(), size: size) + + return size + } +} + private final class ItemNode: ASDisplayNode { private let pressed: () -> Void + private let requestedDeletion: () -> Void private let extractedContainerNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode + private let extractedBackgroundNode: ASImageNode private let titleNode: ImmediateTextNode - private let extractedTitleNode: ImmediateTextNode + private let shortTitleNode: ImmediateTextNode private let badgeContainerNode: ASDisplayNode private let badgeTextNode: ImmediateTextNode private let badgeBackgroundNode: ASImageNode + private var deleteButtonNode: ItemNodeDeleteButtonNode? private let buttonNode: HighlightTrackingButtonNode private var isSelected: Bool = false private(set) var unreadCount: Int = 0 + private var isReordering: Bool = false + private var theme: PresentationTheme? - init(pressed: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) { + init(pressed: @escaping () -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) { self.pressed = pressed + self.requestedDeletion = requestedDeletion self.extractedContainerNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() + self.extractedBackgroundNode = ASImageNode() + self.extractedBackgroundNode.alpha = 0.0 + self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false - self.extractedTitleNode = ImmediateTextNode() - self.extractedTitleNode.displaysAsynchronously = false - self.extractedTitleNode.alpha = 0.0 + self.shortTitleNode = ImmediateTextNode() + self.shortTitleNode.displaysAsynchronously = false + self.shortTitleNode.alpha = 0.0 self.badgeContainerNode = ASDisplayNode() @@ -51,8 +109,9 @@ private final class ItemNode: ASDisplayNode { super.init() + self.extractedContainerNode.contentNode.addSubnode(self.extractedBackgroundNode) self.extractedContainerNode.contentNode.addSubnode(self.titleNode) - self.extractedContainerNode.contentNode.addSubnode(self.extractedTitleNode) + self.extractedContainerNode.contentNode.addSubnode(self.shortTitleNode) self.badgeContainerNode.addSubnode(self.badgeBackgroundNode) self.badgeContainerNode.addSubnode(self.badgeTextNode) self.extractedContainerNode.contentNode.addSubnode(self.badgeContainerNode) @@ -75,8 +134,15 @@ private final class ItemNode: ASDisplayNode { guard let strongSelf = self else { return } - transition.updateAlpha(node: strongSelf.titleNode, alpha: isExtracted ? 0.0 : 1.0) - transition.updateAlpha(node: strongSelf.extractedTitleNode, alpha: isExtracted ? 1.0 : 0.0) + + if isExtracted, let theme = strongSelf.theme { + strongSelf.extractedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: theme.contextMenu.backgroundColor) + } + transition.updateAlpha(node: strongSelf.extractedBackgroundNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.extractedBackgroundNode.image = nil + } + }) } } @@ -84,30 +150,73 @@ private final class ItemNode: ASDisplayNode { self.pressed() } - func updateText(title: String, unreadCount: Int, isNoFilter: Bool, isSelected: Bool, presentationData: PresentationData) { + func updateText(title: String, shortTitle: String, unreadCount: Int, isNoFilter: Bool, isSelected: Bool, isEditing: Bool, isAllChats: Bool, isReordering: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { if self.theme !== presentationData.theme { self.theme = presentationData.theme self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.list.itemCheckColors.fillColor) } - self.containerNode.isGestureEnabled = !isNoFilter + self.containerNode.isGestureEnabled = !isNoFilter && !isEditing && !isReordering + self.buttonNode.isUserInteractionEnabled = !isEditing && !isReordering self.isSelected = isSelected self.unreadCount = unreadCount + transition.updateAlpha(node: self.containerNode, alpha: isReordering && isAllChats ? 0.5 : 1.0) + + if isReordering && !isAllChats { + if self.deleteButtonNode == nil { + let deleteButtonNode = ItemNodeDeleteButtonNode(pressed: { [weak self] in + self?.requestedDeletion() + }) + self.extractedContainerNode.contentNode.addSubnode(deleteButtonNode) + self.deleteButtonNode = deleteButtonNode + if case .animated = transition { + deleteButtonNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.25) + deleteButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + } else if let deleteButtonNode = self.deleteButtonNode { + self.deleteButtonNode = nil + transition.updateTransformScale(node: deleteButtonNode, scale: 0.1) + transition.updateAlpha(node: deleteButtonNode, alpha: 0.0, completion: { [weak deleteButtonNode] _ in + deleteButtonNode?.removeFromSupernode() + }) + } + + transition.updateAlpha(node: self.badgeContainerNode, alpha: (isReordering || unreadCount == 0) ? 0.0 : 1.0) + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) - self.extractedTitleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.contextMenu.extractedContentTintColor) + self.shortTitleNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) if unreadCount != 0 { self.badgeTextNode.attributedText = NSAttributedString(string: "\(unreadCount)", font: Font.regular(14.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor) } + + if self.isReordering != isReordering { + self.isReordering = isReordering + if self.isReordering && !isAllChats { + self.startShaking() + } else { + self.layer.removeAnimation(forKey: "shaking_position") + self.layer.removeAnimation(forKey: "shaking_rotation") + } + } } - func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> (width: CGFloat, shortWidth: CGFloat) { let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) - let _ = self.extractedTitleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) - self.extractedTitleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + + let shortTitleSize = self.shortTitleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + self.shortTitleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - shortTitleSize.height) / 2.0)), size: shortTitleSize) + + if let deleteButtonNode = self.deleteButtonNode { + if let theme = self.theme { + let deleteButtonSize = deleteButtonNode.update(theme: theme) + deleteButtonNode.frame = CGRect(origin: CGPoint(x: -deleteButtonSize.width, y: 5.0), size: deleteButtonSize) + } + } let badgeSize = self.badgeTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) let badgeInset: CGFloat = 4.0 @@ -116,21 +225,35 @@ private final class ItemNode: ASDisplayNode { self.badgeBackgroundNode.frame = CGRect(origin: CGPoint(), size: badgeBackgroundFrame.size) self.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((badgeBackgroundFrame.width - badgeSize.width) / 2.0), y: floor((badgeBackgroundFrame.height - badgeSize.height) / 2.0)), size: badgeSize) - if self.unreadCount == 0 { - self.badgeContainerNode.alpha = 0.0 - return titleSize.width + let width: CGFloat + if self.unreadCount == 0 || self.isReordering { + if !self.isReordering { + self.badgeContainerNode.alpha = 0.0 + } + width = titleSize.width } else { - self.badgeContainerNode.alpha = 1.0 - return badgeBackgroundFrame.maxX + if !self.isReordering { + self.badgeContainerNode.alpha = 1.0 + } + width = badgeBackgroundFrame.maxX } + + let extractedBackgroundHeight: CGFloat = 36.0 + let extractedBackgroundInset: CGFloat = 14.0 + self.extractedBackgroundNode.frame = CGRect(origin: CGPoint(x: -extractedBackgroundInset, y: floor((height - extractedBackgroundHeight) / 2.0)), size: CGSize(width: width + extractedBackgroundInset * 2.0, height: extractedBackgroundHeight)) + + return (width, shortTitleSize.width) } - func updateArea(size: CGSize, sideInset: CGFloat) { + func updateArea(size: CGSize, sideInset: CGFloat, useShortTitle: Bool, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.titleNode, alpha: useShortTitle ? 0.0 : 1.0) + transition.updateAlpha(node: self.shortTitleNode, alpha: useShortTitle ? 1.0 : 0.0) + self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height)) self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundNode.frame.minX, y: 0.0), size: CGSize(width:self.extractedBackgroundNode.frame.width, height: size.height)) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -sideInset, bottom: 0.0, right: -sideInset) @@ -140,17 +263,75 @@ private final class ItemNode: ASDisplayNode { } func animateBadgeIn() { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - self.badgeContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1) - transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0) + if !self.isReordering { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + self.badgeContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1) + transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0) + } } func animateBadgeOut() { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - self.badgeContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) - ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0) - transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1) + if !self.isReordering { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + self.badgeContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0) + transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1) + } + } + + private func startShaking() { + func degreesToRadians(_ x: CGFloat) -> CGFloat { + return .pi * x / 180.0 + } + + let duration: Double = 0.4 + let displacement: CGFloat = 1.0 + let degreesRotation: CGFloat = 2.0 + + let negativeDisplacement = -1.0 * displacement + let position = CAKeyframeAnimation.init(keyPath: "position") + position.beginTime = 0.8 + position.duration = duration + position.values = [ + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: 0, y: 0)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)), + NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)) + ] + position.calculationMode = .linear + position.isRemovedOnCompletion = false + position.repeatCount = Float.greatestFiniteMagnitude + position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + position.isAdditive = true + + let transform = CAKeyframeAnimation.init(keyPath: "transform") + transform.beginTime = 2.6 + transform.duration = 0.3 + transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) + transform.values = [ + degreesToRadians(-1.0 * degreesRotation), + degreesToRadians(degreesRotation), + degreesToRadians(-1.0 * degreesRotation) + ] + transform.calculationMode = .linear + transform.isRemovedOnCompletion = false + transform.repeatCount = Float.greatestFiniteMagnitude + transform.isAdditive = true + transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + + self.layer.add(position, forKey: "shaking_position") + self.layer.add(transform, forKey: "shaking_rotation") + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let deleteButtonNode = self.deleteButtonNode { + if deleteButtonNode.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + return deleteButtonNode.view + } + } + return super.hitTest(point, with: event) } } @@ -180,39 +361,13 @@ enum ChatListFilterTabEntry: Equatable { return filter.text } } -} - -private final class AddItemNode: HighlightableButtonNode { - private let iconNode: ASImageNode - var pressed: (() -> Void)? - - private var theme: PresentationTheme? - - override init() { - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - - super.init() - - self.addSubnode(self.iconNode) - - self.addTarget(self, action: #selector(self.onPressed), forControlEvents: .touchUpInside) - } - - @objc private func onPressed() { - self.pressed?() - } - - func update(size: CGSize, theme: PresentationTheme) { - if self.theme !== theme { - self.theme = theme - self.iconNode.image = PresentationResourcesItemList.plusIconImage(theme) - } - - if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + func shortTitle(strings: PresentationStrings) -> String { + switch self { + case .all: + return "All" + case let .filter(filter): + return filter.text } } } @@ -221,13 +376,32 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { private let scrollNode: ASScrollNode private let selectedLineNode: ASImageNode private var itemNodes: [ChatListFilterTabEntryId: ItemNode] = [:] - private let addNode: AddItemNode var tabSelected: ((ChatListFilterTabEntryId) -> Void)? + var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? var addFilter: (() -> Void)? var contextGesture: ((Int32, ContextExtractedContentContainingNode, ContextGesture) -> Void)? - private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, presentationData: PresentationData)? + private var reorderingGesture: ReorderingGestureRecognizer? + private var reorderingItem: ChatListFilterTabEntryId? + private var reorderingItemPosition: (initial: CGFloat, offset: CGFloat)? + private var reorderingAutoScrollAnimator: ConstantDisplayLinkAnimator? + private var reorderedItemIds: [ChatListFilterTabEntryId]? + + private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData)? + + var reorderedFilterIds: [Int32]? { + return self.reorderedItemIds.flatMap { + $0.compactMap { + switch $0 { + case .all: + return nil + case let .filter(id): + return id + } + } + } + } override init() { self.scrollNode = ASScrollNode() @@ -236,8 +410,6 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { self.selectedLineNode.displaysAsynchronously = false self.selectedLineNode.displayWithoutProcessing = true - self.addNode = AddItemNode() - super.init() self.scrollNode.view.showsHorizontalScrollIndicator = false @@ -250,11 +422,118 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { self.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.selectedLineNode) - self.scrollNode.addSubnode(self.addNode) - self.addNode.pressed = { [weak self] in - self?.addFilter?() - } + let reorderingGesture = ReorderingGestureRecognizer(shouldBegin: { [weak self] point in + guard let strongSelf = self else { + return false + } + for (id, itemNode) in strongSelf.itemNodes { + if itemNode.view.convert(itemNode.bounds, to: strongSelf.view).contains(point) { + if case .all = id { + return false + } + return true + } + } + return false + }, began: { [weak self] point in + guard let strongSelf = self, let _ = strongSelf.currentParams else { + return + } + for (id, itemNode) in strongSelf.itemNodes { + let itemFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.view) + if itemFrame.contains(point) { + strongSelf.reorderingItem = id + itemNode.frame = itemFrame + strongSelf.reorderingAutoScrollAnimator = ConstantDisplayLinkAnimator(update: { + guard let strongSelf = self, let currentLocation = strongSelf.reorderingGesture?.currentLocation else { + return + } + let edgeWidth: CGFloat = 20.0 + if currentLocation.x <= edgeWidth { + var contentOffset = strongSelf.scrollNode.view.contentOffset + contentOffset.x = max(0.0, contentOffset.x - 3.0) + strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false) + } else if currentLocation.x >= strongSelf.bounds.width - edgeWidth { + var contentOffset = strongSelf.scrollNode.view.contentOffset + contentOffset.x = max(0.0, min(strongSelf.scrollNode.view.contentSize.width - strongSelf.scrollNode.bounds.width, contentOffset.x + 3.0)) + strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false) + } + }) + strongSelf.reorderingAutoScrollAnimator?.isPaused = false + strongSelf.addSubnode(itemNode) + + strongSelf.reorderingItemPosition = (itemNode.frame.minX, 0.0) + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + return + } + } + }, ended: { [weak self] in + guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else { + return + } + if let itemNode = strongSelf.itemNodes[reorderingItem] { + let projectedItemFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.scrollNode.view) + itemNode.frame = projectedItemFrame + strongSelf.scrollNode.addSubnode(itemNode) + } + + strongSelf.reorderingItem = nil + strongSelf.reorderingItemPosition = nil + strongSelf.reorderingAutoScrollAnimator?.invalidate() + strongSelf.reorderingAutoScrollAnimator = nil + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + }, moved: { [weak self] offset in + guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else { + return + } + if let reorderingItemNode = strongSelf.itemNodes[reorderingItem], let (initial, _) = strongSelf.reorderingItemPosition, let reorderedItemIds = strongSelf.reorderedItemIds, let currentItemIndex = reorderedItemIds.firstIndex(of: reorderingItem) { + + for (id, itemNode) in strongSelf.itemNodes { + guard let itemIndex = reorderedItemIds.firstIndex(of: id) else { + continue + } + if id != reorderingItem { + let itemFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.view) + if reorderingItemNode.frame.intersects(itemFrame) { + let targetIndex: Int + if reorderingItemNode.frame.midX < itemFrame.midX { + targetIndex = max(1, itemIndex - 1) + } else { + targetIndex = max(1, min(reorderedItemIds.count - 1, itemIndex)) + } + if targetIndex != currentItemIndex { + var updatedReorderedItemIds = reorderedItemIds + if targetIndex > currentItemIndex { + updatedReorderedItemIds.insert(reorderingItem, at: targetIndex + 1) + updatedReorderedItemIds.remove(at: currentItemIndex) + } else { + updatedReorderedItemIds.remove(at: currentItemIndex) + updatedReorderedItemIds.insert(reorderingItem, at: targetIndex) + } + strongSelf.reorderedItemIds = updatedReorderedItemIds + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + } + break + } + } + } + + strongSelf.reorderingItemPosition = (initial, offset) + } + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .immediate) + } + }) + self.reorderingGesture = reorderingGesture + self.view.addGestureRecognizer(reorderingGesture) + reorderingGesture.isEnabled = false } private var previousSelectedAbsFrame: CGRect? @@ -265,9 +544,10 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { self.scrollNode.layer.removeAllAnimations() } - func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, transitionFraction: CGFloat, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter let previousScrollBounds = self.scrollNode.bounds + let previousContentWidth = self.scrollNode.view.contentSize.width if self.currentParams?.presentationData.theme !== presentationData.theme { self.selectedLineNode.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in @@ -277,7 +557,30 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { })?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 1) } - self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, presentationData: presentationData) + if isReordering { + if let reorderedItemIds = self.reorderedItemIds { + let currentIds = Set(reorderedItemIds) + if currentIds != Set(filters.map { $0.id }) { + var updatedReorderedItemIds = reorderedItemIds.filter { id in + return filters.contains(where: { $0.id == id }) + } + for filter in filters { + if !currentIds.contains(filter.id) { + updatedReorderedItemIds.append(filter.id) + } + } + self.reorderedItemIds = updatedReorderedItemIds + } + } else { + self.reorderedItemIds = filters.map { $0.id } + } + } else if self.reorderedItemIds != nil { + self.reorderedItemIds = nil + } + + self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering, isEditing, transitionFraction, presentationData: presentationData) + + self.reorderingGesture?.isEnabled = isEditing || isReordering transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) @@ -288,7 +591,18 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { var badgeAnimations: [ChatListFilterTabEntryId: BadgeAnimation] = [:] - for filter in filters { + var reorderedFilters: [ChatListFilterTabEntry] = filters + if let reorderedItemIds = self.reorderedItemIds { + reorderedFilters = reorderedItemIds.compactMap { id -> ChatListFilterTabEntry? in + if let index = filters.firstIndex(where: { $0.id == id }) { + return filters[index] + } else { + return nil + } + } + } + + for filter in reorderedFilters { let itemNode: ItemNode var wasAdded = false if let current = self.itemNodes[filter.id] { @@ -297,6 +611,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { wasAdded = true itemNode = ItemNode(pressed: { [weak self] in self?.tabSelected?(filter.id) + }, requestedDeletion: { [weak self] in + self?.tabRequestedDeletion?(filter.id) }, contextGesture: { [weak self] sourceNode, gesture in guard let strongSelf = self else { return @@ -325,7 +641,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { if !wasAdded && (itemNode.unreadCount != 0) != (unreadCount != 0) { badgeAnimations[filter.id] = (unreadCount != 0) ? .in : .out } - itemNode.updateText(title: filter.title(strings: presentationData.strings), unreadCount: unreadCount, isNoFilter: isNoFilter, isSelected: selectedFilter == filter.id, presentationData: presentationData) + itemNode.updateText(title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, isNoFilter: isNoFilter, isSelected: selectedFilter == filter.id, isEditing: false, isAllChats: isNoFilter, isReordering: isEditing || isReordering, presentationData: presentationData, transition: transition) } var removeKeys: [ChatListFilterTabEntryId] = [] for (id, _) in self.itemNodes { @@ -335,15 +651,18 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { } for id in removeKeys { if let itemNode = self.itemNodes.removeValue(forKey: id) { - itemNode.removeFromSupernode() + transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + transition.updateTransformScale(node: itemNode, scale: 0.1) } } - var tabSizes: [(CGSize, ItemNode, Bool)] = [] + var tabSizes: [(ChatListFilterTabEntryId, CGSize, CGSize, ItemNode, Bool)] = [] var totalRawTabSize: CGFloat = 0.0 var selectionFrames: [CGRect] = [] - for filter in filters { + for filter in reorderedFilters { guard let itemNode = self.itemNodes[filter.id] else { continue } @@ -351,9 +670,10 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { if wasAdded { self.scrollNode.addSubnode(itemNode) } - let paneNodeWidth = itemNode.updateLayout(height: size.height, transition: transition) + let (paneNodeWidth, paneNodeShortWidth) = itemNode.updateLayout(height: size.height, transition: transition) let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height) - tabSizes.append((paneNodeSize, itemNode, wasAdded)) + let paneNodeShortSize = CGSize(width: paneNodeShortWidth, height: size.height) + tabSizes.append((filter.id, paneNodeSize, paneNodeShortSize, itemNode, wasAdded)) totalRawTabSize += paneNodeSize.width if case .animated = transition, let badgeAnimation = badgeAnimations[filter.id] { @@ -370,35 +690,56 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { let sideInset: CGFloat = 16.0 var leftOffset: CGFloat = sideInset + + var longTitlesWidth: CGFloat = sideInset for i in 0 ..< tabSizes.count { - let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] - let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) - if wasAdded { - paneNode.frame = paneFrame - paneNode.alpha = 0.0 - transition.updateAlpha(node: paneNode, alpha: 1.0) - } else { - transition.updateFrameAdditive(node: paneNode, frame: paneFrame) + let (itemId, paneNodeSize, paneNodeShortSize, paneNode, wasAdded) = tabSizes[i] + longTitlesWidth += paneNodeSize.width + if i != tabSizes.count - 1 { + longTitlesWidth += minSpacing } - paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0) + } + longTitlesWidth += sideInset + let useShortTitles = longTitlesWidth > size.width + + for i in 0 ..< tabSizes.count { + let (itemId, paneNodeLongSize, paneNodeShortSize, paneNode, wasAdded) = tabSizes[i] + let useShortTitle = itemId == .all && useShortTitles + let paneNodeSize = useShortTitle ? paneNodeShortSize : paneNodeLongSize + + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + + if itemId == self.reorderingItem, let (initial, offset) = self.reorderingItemPosition { + transition.updateSublayerTransformScale(node: paneNode, scale: 1.2) + transition.updateAlpha(node: paneNode, alpha: 0.9) + transition.updateFrameAdditive(node: paneNode, frame: CGRect(origin: CGPoint(x: initial + offset, y: paneFrame.minY), size: paneFrame.size)) + } else { + transition.updateSublayerTransformScale(node: paneNode, scale: 1.0) + transition.updateAlpha(node: paneNode, alpha: 1.0) + if wasAdded { + paneNode.frame = paneFrame + paneNode.alpha = 0.0 + transition.updateAlpha(node: paneNode, alpha: 1.0) + } else { + transition.updateFrameAdditive(node: paneNode, frame: paneFrame) + } + } + paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0, useShortTitle: useShortTitle, transition: transition) paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing / 2.0, bottom: 0.0, right: -minSpacing / 2.0) selectionFrames.append(paneFrame) leftOffset += paneNodeSize.width + minSpacing } + leftOffset -= minSpacing + leftOffset += sideInset - let addSize = CGSize(width: 32.0, height: size.height) - transition.updateFrame(node: self.addNode, frame: CGRect(origin: CGPoint(x: max(leftOffset, size.width - sideInset - addSize.width + 6.0), y: 0.0), size: addSize)) - self.addNode.update(size: addSize, theme: presentationData.theme) - leftOffset += addSize.width + minSpacing - - self.scrollNode.view.contentSize = CGSize(width: leftOffset - minSpacing + sideInset - 5.0, height: size.height) + self.scrollNode.view.contentSize = CGSize(width: leftOffset, height: size.height) var previousFrame: CGRect? var nextFrame: CGRect? var selectedFrame: CGRect? - if let selectedFilter = selectedFilter, let currentIndex = filters.firstIndex(where: { $0.id == selectedFilter }) { + if let selectedFilter = selectedFilter, let currentIndex = reorderedFilters.firstIndex(where: { $0.id == selectedFilter }) { func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t))) } @@ -423,28 +764,33 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { if wasAdded { self.selectedLineNode.frame = lineFrame self.selectedLineNode.alpha = 0.0 - transition.updateAlpha(node: self.selectedLineNode, alpha: 1.0) } else { transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) } - if !transitionFraction.isZero { + transition.updateAlpha(node: self.selectedLineNode, alpha: isReordering && selectedFilter == .all ? 0.5 : 1.0) + + //if !transitionFraction.isZero { if let previousSelectedFrame = self.previousSelectedFrame, abs(previousSelectedFrame.offsetBy(dx: -previousScrollBounds.minX, dy: 0.0).midX - previousScrollBounds.width / 2.0) < 1.0 { - let previousContentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(previousSelectedFrame.midX - self.scrollNode.bounds.width / 2.0))) + let previousContentOffsetX = max(0.0, min(previousContentWidth - self.scrollNode.bounds.width, floor(previousSelectedFrame.midX - self.scrollNode.bounds.width / 2.0))) if abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 { focusOnSelectedFilter = true } } - } - if focusOnSelectedFilter { - if transitionFraction.isZero && selectedFilter == filters.first?.id { - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) - } else if transitionFraction.isZero && selectedFilter == filters.last?.id { - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size)) + //} + if focusOnSelectedFilter && self.reorderingItem == nil { + let updatedBounds: CGRect + if transitionFraction.isZero && selectedFilter == reorderedFilters.first?.id { + updatedBounds = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) + } else if transitionFraction.isZero && selectedFilter == reorderedFilters.last?.id { + updatedBounds = CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size) } else { let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0))) - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)) + updatedBounds = CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size) } - } else if !wasAdded, transitionFraction.isZero, let previousSelectedAbsFrame = self.previousSelectedAbsFrame { + self.scrollNode.bounds = updatedBounds + } + transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX) + /*else if false, !wasAdded, transitionFraction.isZero, let previousSelectedAbsFrame = self.previousSelectedAbsFrame { let contentOffsetX: CGFloat if previousScrollBounds.minX.isZero { contentOffsetX = 0.0 @@ -454,7 +800,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, selectedFrame.midX - previousSelectedAbsFrame.midX)) } transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)) - } + }*/ self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0) self.previousSelectedFrame = selectedFrame } else { @@ -464,3 +810,131 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { } } } + +private class ReorderingGestureRecognizerTimerTarget: NSObject { + private let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + + super.init() + } + + @objc func timerEvent() { + self.f() + } +} + +private final class ReorderingGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + private let shouldBegin: (CGPoint) -> Bool + private let began: (CGPoint) -> Void + private let ended: () -> Void + private let moved: (CGFloat) -> Void + + private var initialLocation: CGPoint? + private var delayTimer: Foundation.Timer? + + var currentLocation: CGPoint? + + init(shouldBegin: @escaping (CGPoint) -> Bool, began: @escaping (CGPoint) -> Void, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) { + self.shouldBegin = shouldBegin + self.began = began + self.ended = ended + self.moved = moved + + super.init(target: nil, action: nil) + + self.delegate = self + } + + override func reset() { + super.reset() + + self.initialLocation = nil + self.delayTimer?.invalidate() + self.delayTimer = nil + self.currentLocation = nil + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if otherGestureRecognizer is UIPanGestureRecognizer { + return true + } else { + return false + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + guard let location = touches.first?.location(in: self.view) else { + self.state = .failed + return + } + + if self.state == .possible { + if self.delayTimer == nil { + if !self.shouldBegin(location) { + self.state = .failed + return + } + self.initialLocation = location + let timer = Foundation.Timer(timeInterval: 0.2, target: ReorderingGestureRecognizerTimerTarget { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.delayTimer = nil + strongSelf.state = .began + strongSelf.began(location) + }, selector: #selector(ReorderingGestureRecognizerTimerTarget.timerEvent), userInfo: nil, repeats: false) + self.delayTimer = timer + RunLoop.main.add(timer, forMode: .common) + } else { + self.state = .failed + } + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + if self.state == .began || self.state == .changed { + self.delayTimer?.invalidate() + self.ended() + self.state = .failed + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + if self.state == .began || self.state == .changed { + self.delayTimer?.invalidate() + self.ended() + self.state = .failed + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + guard let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) else { + return + } + let offset = location.x - initialLocation.x + self.currentLocation = location + + if self.delayTimer != nil { + if abs(offset) > 4.0 { + self.delayTimer?.invalidate() + self.state = .failed + return + } + } else { + if self.state == .began || self.state == .changed { + self.state = .changed + self.moved(offset) + } + } + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index b52d466e65..a5ac5daeae 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -32,7 +32,11 @@ struct ChatListNodeViewUpdate { func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredicate { let includePeers = Set(filter.includePeers) let excludePeers = Set(filter.excludePeers) - return ChatListFilterPredicate(includePeerIds: includePeers, excludePeerIds: excludePeers, include: { peer, notificationSettings, isUnread, isContact, isArchived in + var includeAdditionalPeerGroupIds: [PeerGroupId] = [] + if !filter.excludeArchived { + includeAdditionalPeerGroupIds.append(Namespaces.PeerGroup.archive) + } + return ChatListFilterPredicate(includePeerIds: includePeers, excludePeerIds: excludePeers, includeAdditionalPeerGroupIds: includeAdditionalPeerGroupIds, include: { peer, notificationSettings, isUnread, isContact in if filter.excludeRead { if !isUnread { return false @@ -47,11 +51,6 @@ func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredic return false } } - if filter.excludeArchived { - if isArchived { - return false - } - } if !filter.categories.contains(.contacts) && isContact { if let user = peer as? TelegramUser { if user.botInfo == nil { @@ -77,23 +76,12 @@ func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredic } } } - if !filter.categories.contains(.smallGroups) { + if !filter.categories.contains(.groups) { if let _ = peer as? TelegramGroup { return false } else if let channel = peer as? TelegramChannel { if case .group = channel.info { - if channel.username == nil { - return false - } - } - } - } - if !filter.categories.contains(.largeGroups) { - if let channel = peer as? TelegramChannel { - if case .group = channel.info { - if channel.username != nil { - return false - } + return false } } } diff --git a/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift index b547630872..184dc615f3 100644 --- a/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift +++ b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift @@ -101,12 +101,9 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt if filter.data.categories.contains(.nonContacts) { tags.append(.nonContact) } - if filter.data.categories.contains(.smallGroups) { + if filter.data.categories.contains(.groups) { tags.append(.smallGroup) } - if filter.data.categories.contains(.largeGroups) { - tags.append(.largeGroup) - } if filter.data.categories.contains(.bots) { tags.append(.bot) } diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index ee711d5241..f8fc1cd46d 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -465,7 +465,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { case .none: updatedSelectionNode = nil case let .selectable(selected): - rightInset += 28.0 + rightInset += 38.0 isSelected = selected let selectionNode: CheckNode diff --git a/submodules/Postbox/Sources/ChatListTable.swift b/submodules/Postbox/Sources/ChatListTable.swift index e27cd13c26..dc3ad0e0a4 100644 --- a/submodules/Postbox/Sources/ChatListTable.swift +++ b/submodules/Postbox/Sources/ChatListTable.swift @@ -1,7 +1,7 @@ import Foundation enum ChatListOperation { - case InsertEntry(ChatListIndex, IntermediateMessage?, CombinedPeerReadState?, PeerChatListEmbeddedInterfaceState?) + case InsertEntry(ChatListIndex, MessageIndex?) case InsertHole(ChatListHole) case RemoveEntry([ChatListIndex]) case RemoveHoles([ChatListIndex]) @@ -22,15 +22,15 @@ enum ChatListEntryInfo { } enum ChatListIntermediateEntry { - case message(ChatListIndex, IntermediateMessage?, PeerChatListEmbeddedInterfaceState?) + case message(ChatListIndex, MessageIndex?) case hole(ChatListHole) var index: ChatListIndex { switch self { - case let .message(index, _, _): - return index - case let .hole(hole): - return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) + case let .message(index, _): + return index + case let .hole(hole): + return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) } } } @@ -78,12 +78,12 @@ private func extractKey(_ key: ValueBoxKey) -> (groupId: PeerGroupId, pinningInd ) } -private func readEntry(groupId: PeerGroupId, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, key: ValueBoxKey, value: ReadBuffer) -> ChatListIntermediateEntry { +private func readEntry(groupId: PeerGroupId, key: ValueBoxKey, value: ReadBuffer) -> ChatListIntermediateEntry { let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key) assert(groupId == keyGroupId) let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex) if type == ChatListEntryType.message.rawValue { - var message: IntermediateMessage? + var messageIndex: MessageIndex? if value.length != 0 { var idNamespace: Int32 = 0 value.read(&idNamespace, offset: 0, length: 4) @@ -92,9 +92,9 @@ private func readEntry(groupId: PeerGroupId, messageHistoryTable: MessageHistory var indexTimestamp: Int32 = 0 value.read(&indexTimestamp, offset: 0, length: 4) - message = messageHistoryTable.getMessage(MessageIndex(id: MessageId(peerId: index.messageIndex.id.peerId, namespace: idNamespace, id: idId), timestamp: indexTimestamp)) + messageIndex = MessageIndex(id: MessageId(peerId: index.messageIndex.id.peerId, namespace: idNamespace, id: idId), timestamp: indexTimestamp) } - return .message(index, message, peerChatInterfaceStateTable.get(index.messageIndex.id.peerId)?.chatListEmbeddedState) + return .message(index, messageIndex) } else if type == ChatListEntryType.hole.rawValue { return .hole(ChatListHole(index: index.messageIndex)) } else { @@ -203,9 +203,9 @@ final class ChatListTable: Table { var itemIds: [(id: PinnedItemId, rank: Int)] = [] self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.key(groupId: groupId, index: ChatListIndex(pinningIndex: UInt16.max - 1, messageIndex: MessageIndex.absoluteUpperBound()), type: .message).successor, values: { key, value in let keyIndex = extractKey(key) - let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + let entry = readEntry(groupId: groupId, key: key, value: value) switch entry { - case let .message(index, _, _): + case let .message(index, _): itemIds.append((.peer(index.messageIndex.id.peerId), Int(keyIndex.pinningIndex ?? 0))) default: break @@ -256,7 +256,7 @@ final class ChatListTable: Table { if let peer = postbox.peerTable.get(messageIndex.id.peerId) { let isUnread = postbox.readStateTable.getCombinedState(messageIndex.id.peerId)?.isUnread ?? false let isContact = postbox.contactsTable.isContact(peerId: messageIndex.id.peerId) - if filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(messageIndex.id.peerId), isUnread: isUnread, isContact: isContact, isArchived: groupId != .root) { + if filterPredicate.includes(peer: peer, groupId: groupId, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(messageIndex.id.peerId), isUnread: isUnread, isContact: isContact) { passFilter = true } else { passFilter = false @@ -306,7 +306,7 @@ final class ChatListTable: Table { self.ensureInitialized(groupId: groupId) } - let topMessage = messageHistoryTable.topMessage(peerId) + let topMessage = messageHistoryTable.topIndex(peerId: peerId) let embeddedChatState = peerChatInterfaceStateTable.get(peerId)?.chatListEmbeddedState let rawTopMessageIndex: MessageIndex? @@ -339,7 +339,7 @@ final class ChatListTable: Table { addOperation(.RemoveEntry([currentOrderingIndex]), groupId: currentGroupId, to: &operations) } self.justInsertIndex(groupId: updatedGroupId, index: updatedOrderingIndex, topMessageIndex: rawTopMessageIndex) - addOperation(.InsertEntry(updatedOrderingIndex, topMessage, messageHistoryTable.readStateTable.getCombinedState(peerId), embeddedChatState), groupId: updatedGroupId, to: &operations) + addOperation(.InsertEntry(updatedOrderingIndex, topMessage), groupId: updatedGroupId, to: &operations) } else { if let (currentGroupId, currentOrderingIndex) = currentGroupAndIndex { self.justRemoveMessageIndex(groupId: currentGroupId, index: currentOrderingIndex) @@ -410,7 +410,7 @@ final class ChatListTable: Table { var upper: ChatListIntermediateEntry? self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: index, type: .message), end: self.lowerBound(groupId: groupId), values: { key, value in - let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + let entry = readEntry(groupId: groupId, key: key, value: value) if let predicate = predicate { if predicate(entry) { lowerEntries.append(entry) @@ -429,7 +429,7 @@ final class ChatListTable: Table { } self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: index, type: .message).predecessor, end: self.upperBound(groupId: groupId), values: { key, value in - let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + let entry = readEntry(groupId: groupId, key: key, value: value) if let predicate = predicate { if predicate(entry) { upperEntries.append(entry) @@ -519,6 +519,31 @@ final class ChatListTable: Table { } return result } + + func entries(groupId: PeerGroupId, from fromIndex: (ChatListIndex, Bool), to toIndex: (ChatListIndex, Bool), peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> [ChatListIntermediateEntry] { + self.ensureInitialized(groupId: groupId) + + var entries: [ChatListIntermediateEntry] = [] + let fromKey = self.key(groupId: groupId, index: fromIndex.0, type: fromIndex.1 ? .message : .hole) + let toKey = self.key(groupId: groupId, index: toIndex.0, type: toIndex.1 ? .message : .hole) + + self.valueBox.filteredRange(self.table, start: fromKey, end: toKey, values: { key, value in + let entry = readEntry(groupId: groupId, key: key, value: value) + if let predicate = predicate { + if predicate(entry) { + entries.append(entry) + return .accept + } else { + return .skip + } + } else { + entries.append(entry) + return .accept + } + }, limit: count) + assert(entries.count <= count) + return entries + } func earlierEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> [ChatListIntermediateEntry] { self.ensureInitialized(groupId: groupId) @@ -532,7 +557,7 @@ final class ChatListTable: Table { } self.valueBox.filteredRange(self.table, start: key, end: self.lowerBound(groupId: groupId), values: { key, value in - let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + let entry = readEntry(groupId: groupId, key: key, value: value) if let predicate = predicate { if predicate(entry) { entries.append(entry) @@ -596,7 +621,7 @@ final class ChatListTable: Table { } self.valueBox.filteredRange(self.table, start: key, end: self.upperBound(groupId: groupId), values: { key, value in - let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + let entry = readEntry(groupId: groupId, key: key, value: value) if let predicate = predicate { if predicate(entry) { entries.append(entry) @@ -640,9 +665,7 @@ final class ChatListTable: Table { break } if let topMessageIndex = index.topMessageIndex { - if let message = messageHistoryTable.getMessage(topMessageIndex) { - return ChatListIntermediateEntry.message(ChatListIndex(pinningIndex: nil, messageIndex: topMessageIndex), message, nil) - } + return ChatListIntermediateEntry.message(ChatListIndex(pinningIndex: nil, messageIndex: topMessageIndex), topMessageIndex) } return nil } @@ -651,7 +674,7 @@ final class ChatListTable: Table { if let (peerGroupId, index) = self.getPeerChatListIndex(peerId: peerId), peerGroupId == groupId { let key = self.key(groupId: groupId, index: index, type: .message) if let value = self.valueBox.get(self.table, key: key) { - return readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + return readEntry(groupId: groupId, key: key, value: value) } else { return nil } @@ -664,7 +687,7 @@ final class ChatListTable: Table { if let (peerGroupId, index) = self.getPeerChatListIndex(peerId: peerId) { let key = self.key(groupId: peerGroupId, index: index, type: .message) if let value = self.valueBox.get(self.table, key: key) { - return readEntry(groupId: peerGroupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + return readEntry(groupId: peerGroupId, key: key, value: value) } else { return nil } @@ -705,10 +728,7 @@ final class ChatListTable: Table { func allPeerIds(groupId: PeerGroupId) -> [PeerId] { var peerIds: [PeerId] = [] self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), keys: { key in - let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key) - assert(groupId == keyGroupId) - - let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex) + let (_, _, messageIndex, type) = extractKey(key) if type == ChatListEntryType.message.rawValue { peerIds.append(messageIndex.id.peerId) } diff --git a/submodules/Postbox/Sources/ChatListView.swift b/submodules/Postbox/Sources/ChatListView.swift index 7fc54b0b40..d823aa7b19 100644 --- a/submodules/Postbox/Sources/ChatListView.swift +++ b/submodules/Postbox/Sources/ChatListView.swift @@ -165,9 +165,9 @@ public enum ChatListEntry: Comparable { } } -private func processedChatListEntry(_ entry: MutableChatListEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) -> MutableChatListEntry { +/*private func processedChatListEntry(_ entry: MutableChatListEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) -> MutableChatListEntry { switch entry { - case let .IntermediateMessageEntry(index, message, readState, embeddedState): + case let .IntermediateMessageEntry(index, messageIndex): var updatedMessage = message if let message = message, let cachedData = cachedDataTable.get(message.id.peerId), let associatedHistoryMessageId = cachedData.associatedHistoryMessageId, message.id.id == 1 { if let messageIndex = messageHistoryTable.messageHistoryIndexTable.earlierEntries(id: associatedHistoryMessageId, count: 1).first { @@ -180,17 +180,17 @@ private func processedChatListEntry(_ entry: MutableChatListEntry, cachedDataTab default: return entry } -} +}*/ enum MutableChatListEntry: Equatable { - case IntermediateMessageEntry(ChatListIndex, IntermediateMessage?, CombinedPeerReadState?, PeerChatListEmbeddedInterfaceState?) - case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo, Bool, Bool) + case IntermediateMessageEntry(index: ChatListIndex, messageIndex: MessageIndex?) + case MessageEntry(index: ChatListIndex, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, tagSummaryInfo: ChatListMessageTagSummaryInfo, hasFailedMessages: Bool, isContact: Bool) case HoleEntry(ChatListHole) init(_ intermediateEntry: ChatListIntermediateEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) { switch intermediateEntry { - case let .message(index, message, embeddedState): - self = processedChatListEntry(.IntermediateMessageEntry(index, message, readStateTable.getCombinedState(index.messageIndex.id.peerId), embeddedState), cachedDataTable: cachedDataTable, readStateTable: readStateTable, messageHistoryTable: messageHistoryTable) + case let .message(index, messageIndex): + self = .IntermediateMessageEntry(index: index, messageIndex: messageIndex) case let .hole(hole): self = .HoleEntry(hole) } @@ -198,8 +198,8 @@ enum MutableChatListEntry: Equatable { var index: ChatListIndex { switch self { - case let .IntermediateMessageEntry(index, _, _, _): - return index + case let .IntermediateMessageEntry(intermediateMessageEntry): + return intermediateMessageEntry.index case let .MessageEntry(index, _, _, _, _, _, _, _, _, _): return index case let .HoleEntry(hole): @@ -254,69 +254,32 @@ private enum ChatListEntryType { case groupReference } -private func updateMessagePeers(_ message: Message, updatedPeers: [PeerId: Peer]) -> Message? { - var updated = false - for (peerId, currentPeer) in message.peers { - if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) { - updated = true - break - } - } - if updated { - var peers = SimpleDictionary() - for (peerId, currentPeer) in message.peers { - if let updatedPeer = updatedPeers[peerId] { - peers[peerId] = updatedPeer - } else { - peers[peerId] = currentPeer - } - } - return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds) - } - return nil -} - -private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [PeerId: Peer]) -> RenderedPeer? { - var updated = false - for (peerId, currentPeer) in renderedPeer.peers { - if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) { - updated = true - break - } - } - if updated { - var peers = SimpleDictionary() - for (peerId, currentPeer) in renderedPeer.peers { - if let updatedPeer = updatedPeers[peerId] { - peers[peerId] = updatedPeer - } else { - peers[peerId] = currentPeer - } - } - return RenderedPeer(peerId: renderedPeer.peerId, peers: peers) - } - return nil -} - public struct ChatListFilterPredicate { public var includePeerIds: Set public var excludePeerIds: Set - public var include: (Peer, PeerNotificationSettings?, Bool, Bool, Bool) -> Bool + public var includeAdditionalPeerGroupIds: [PeerGroupId] + public var include: (Peer, PeerNotificationSettings?, Bool, Bool) -> Bool - public init(includePeerIds: Set, excludePeerIds: Set, include: @escaping (Peer, PeerNotificationSettings?, Bool, Bool, Bool) -> Bool) { + public init(includePeerIds: Set, excludePeerIds: Set, includeAdditionalPeerGroupIds: [PeerGroupId], include: @escaping (Peer, PeerNotificationSettings?, Bool, Bool) -> Bool) { self.includePeerIds = includePeerIds self.excludePeerIds = excludePeerIds + self.includeAdditionalPeerGroupIds = includeAdditionalPeerGroupIds self.include = include } - func includes(peer: Peer, notificationSettings: PeerNotificationSettings?, isUnread: Bool, isContact: Bool, isArchived: Bool) -> Bool { + func includes(peer: Peer, groupId: PeerGroupId, notificationSettings: PeerNotificationSettings?, isUnread: Bool, isContact: Bool) -> Bool { if self.excludePeerIds.contains(peer.id) { return false } if self.includePeerIds.contains(peer.id) { return true } - return self.include(peer, notificationSettings, isUnread, isContact, isArchived) + if groupId != .root { + if !self.includeAdditionalPeerGroupIds.contains(groupId) { + return false + } + } + return self.include(peer, notificationSettings, isUnread, isContact) } } @@ -324,64 +287,48 @@ final class MutableChatListView { let groupId: PeerGroupId let filterPredicate: ChatListFilterPredicate? private let summaryComponents: ChatListEntrySummaryComponents - fileprivate var additionalItemIds: Set - fileprivate var additionalItemEntries: [MutableChatListEntry] - fileprivate var additionalMixedItemIds: Set - fileprivate var additionalMixedPinnedItemIds: Set - fileprivate var additionalMixedItemEntries: [MutableChatListEntry] - fileprivate var additionalMixedPinnedEntries: [MutableChatListEntry] - fileprivate var earlier: MutableChatListEntry? - fileprivate var later: MutableChatListEntry? - fileprivate var entries: [MutableChatListEntry] fileprivate var groupEntries: [ChatListGroupReferenceEntry] private var count: Int + private let spaces: [ChatListViewSpace] + fileprivate var state: ChatListViewState + fileprivate var sampledState: ChatListViewSample + init(postbox: Postbox, groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate?, aroundIndex: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents) { - let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: groupId, index: aroundIndex, count: count, filterPredicate: filterPredicate) - self.groupId = groupId self.filterPredicate = filterPredicate - self.earlier = earlier - self.entries = entries - self.later = later - self.count = count self.summaryComponents = summaryComponents - self.additionalItemEntries = [] - self.additionalMixedItemEntries = [] - self.additionalMixedPinnedEntries = [] - self.additionalMixedItemIds = Set() - self.additionalMixedPinnedItemIds = Set() + + var spaces: [ChatListViewSpace] = [ + .group(groupId: self.groupId, pinned: .notPinned) + ] if let filterPredicate = self.filterPredicate { - self.additionalMixedItemIds.formUnion(filterPredicate.includePeerIds) - for (itemId, _) in postbox.chatListTable.getPinnedItemIds(groupId: self.groupId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) { - switch itemId { - case let .peer(peerId): - self.additionalMixedPinnedItemIds.insert(peerId) - } - } - } - for peerId in self.additionalMixedItemIds { - if let entry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) { - self.additionalMixedItemEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable)) - } - } - for peerId in self.additionalMixedPinnedItemIds { - if let entry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) { - self.additionalMixedPinnedEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable)) + spaces.append(.group(groupId: self.groupId, pinned: .includePinnedAsUnpinned)) + for additionalGroupId in filterPredicate.includeAdditionalPeerGroupIds { + spaces.append(.group(groupId: additionalGroupId, pinned: .notPinned)) + spaces.append(.group(groupId: additionalGroupId, pinned: .includePinnedAsUnpinned)) } + } else { + spaces.append(.group(groupId: self.groupId, pinned: .includePinned)) } + self.spaces = spaces + self.state = ChatListViewState(postbox: postbox, spaces: self.spaces, anchorIndex: aroundIndex, filterPredicate: self.filterPredicate, summaryComponents: self.summaryComponents, halfLimit: count) + self.sampledState = self.state.sample(postbox: postbox) + + self.count = count + if case .root = groupId, self.filterPredicate == nil { - let itemIds = postbox.additionalChatListItemsTable.get() + /*let itemIds = postbox.additionalChatListItemsTable.get() self.additionalItemIds = Set(itemIds) for peerId in itemIds { if let entry = postbox.chatListTable.getStandalone(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable) { self.additionalItemEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable)) } - } + }*/ self.groupEntries = [] self.reloadGroups(postbox: postbox) } else { - self.additionalItemIds = Set() + //self.additionalItemIds = Set() self.groupEntries = [] } } @@ -456,87 +403,31 @@ final class MutableChatListView { } func refreshDueToExternalTransaction(postbox: Postbox) -> Bool { - var index = ChatListIndex.absoluteUpperBound - if !self.entries.isEmpty && self.later != nil { - index = self.entries[self.entries.count / 2].index - } + var updated = false + + self.state = ChatListViewState(postbox: postbox, spaces: self.spaces, anchorIndex: .absoluteUpperBound, filterPredicate: self.filterPredicate, summaryComponents: self.summaryComponents, halfLimit: self.count) + self.sampledState = self.state.sample(postbox: postbox) + updated = true - let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.count, filterPredicate: self.filterPredicate) let currentGroupEntries = self.groupEntries self.reloadGroups(postbox: postbox) - var updated = false - if self.groupEntries != currentGroupEntries { updated = true } - if entries != self.entries || earlier != self.earlier || later != self.later { - self.entries = entries - self.earlier = earlier - self.later = later - updated = true - } - return updated } func replay(postbox: Postbox, operations: [PeerGroupId: [ChatListOperation]], updatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence], transaction: PostboxTransaction, context: MutableChatListViewReplayContext) -> Bool { var hasChanges = false - if let groupOperations = operations[self.groupId] { - for operation in groupOperations { - switch operation { - case let .InsertEntry(index, message, combinedReadState, embeddedState): - if self.add(.IntermediateMessageEntry(index, message, combinedReadState, embeddedState), postbox: postbox) { - hasChanges = true - } - case let .InsertHole(index): - if self.add(.HoleEntry(index), postbox: postbox) { - hasChanges = true - } - case let .RemoveEntry(indices): - if self.remove(Set(indices), type: .message, context: context) { - hasChanges = true - } - case let .RemoveHoles(indices): - if self.remove(Set(indices), type: .hole, context: context) { - hasChanges = true - } - } - } + if self.state.replay(postbox: postbox, transaction: transaction) { + self.sampledState = self.state.sample(postbox: postbox) + hasChanges = true } - /*if let filterPredicate = self.filterPredicate, !filterPredicate.includePeerIds.isEmpty { - for (groupId, groupOperations) in operations { - if groupId == self.groupId { - continue - } - for operation in groupOperations { - switch operation { - case let .InsertEntry(index, message, combinedReadState, embeddedState): - if filterPredicate.includePeerIds.contains(index.messageIndex.id.peerId) { - if self.add(.IntermediateMessageEntry(index, message, combinedReadState, embeddedState), postbox: postbox) { - hasChanges = true - } - } - case .InsertHole: - break - case let .RemoveEntry(indices): - let updatedIndices = indices.filter { index in - return filterPredicate.includePeerIds.contains(index.messageIndex.id.peerId) - } - if !updatedIndices.isEmpty && self.remove(Set(updatedIndices), type: .message, context: context) { - hasChanges = true - } - case .RemoveHoles: - break - } - } - } - }*/ - if case .root = self.groupId, self.filterPredicate == nil { var invalidatedGroups = false for (groupId, groupOperations) in operations { @@ -573,184 +464,9 @@ final class MutableChatListView { } } - if !updatedPeerNotificationSettings.isEmpty { - if let filterPredicate = self.filterPredicate { - for (peerId, settingsChange) in updatedPeerNotificationSettings { - if let peer = postbox.peerTable.get(peerId), !self.additionalMixedItemIds.contains(peerId), !self.additionalMixedPinnedItemIds.contains(peerId) { - let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false - let wasIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.0, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId), isArchived: false) - let isIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.1, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId), isArchived: false) - if wasIncluded != isIncluded { - if isIncluded { - let tableEntry: ChatListIntermediateEntry? - if filterPredicate.includePeerIds.contains(peerId) { - tableEntry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) - } else { - tableEntry = postbox.chatListTable.getEntry(groupId: self.groupId, peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) - } - if let entry = tableEntry { - switch entry { - case let .message(index, message, embeddedState): - let combinedReadState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId) - if self.add(.IntermediateMessageEntry(entry.index, message, combinedReadState, embeddedState), postbox: postbox) { - hasChanges = true - } - default: - break - } - } - } else { - loop: for i in 0 ..< self.entries.count { - switch self.entries[i] { - case .MessageEntry(let index, _, _, _, _, _, _, _, _, _), .IntermediateMessageEntry(let index, _, _, _): - if index.messageIndex.id.peerId == peerId { - self.entries.remove(at: i) - hasChanges = true - break loop - } - default: - break - } - } - } - } - } - } - } - - for i in 0 ..< self.entries.count { - switch self.entries[i] { - case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact): - var notificationSettingsPeerId = peer.peerId - if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId { - notificationSettingsPeerId = associatedPeerId - } - if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] { - self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact) - hasChanges = true - } - default: - continue - } - } - - for i in 0 ..< self.additionalMixedItemEntries.count { - switch self.additionalMixedItemEntries[i] { - case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact): - var notificationSettingsPeerId = peer.peerId - if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId { - notificationSettingsPeerId = associatedPeerId - } - if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] { - self.additionalMixedItemEntries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact) - hasChanges = true - } - default: - continue - } - } - - for i in 0 ..< self.additionalMixedPinnedEntries.count { - switch self.additionalMixedPinnedEntries[i] { - case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact): - var notificationSettingsPeerId = peer.peerId - if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId { - notificationSettingsPeerId = associatedPeerId - } - if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] { - self.additionalMixedPinnedEntries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact) - hasChanges = true - } - default: - continue - } - } - } + /* - if !transaction.updatedFailedMessagePeerIds.isEmpty { - for i in 0 ..< self.entries.count { - switch self.entries[i] { - case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, previousHasFailed, isContact): - if transaction.updatedFailedMessagePeerIds.contains(index.messageIndex.id.peerId) { - let hasFailed = postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId) - if previousHasFailed != hasFailed { - self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact) - hasChanges = true - } - } - default: - continue - } - } - } - if !updatedPeers.isEmpty { - for i in 0 ..< self.entries.count { - switch self.entries[i] { - case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact): - var updatedMessage: Message? - if let message = message { - updatedMessage = updateMessagePeers(message, updatedPeers: updatedPeers) - } - let updatedPeer = updatedRenderedPeer(peer, updatedPeers: updatedPeers) - if updatedMessage != nil || updatedPeer != nil { - self.entries[i] = .MessageEntry(index, updatedMessage ?? message, readState, settings, embeddedState, updatedPeer ?? peer, peerPresence, summaryInfo, hasFailed, isContact) - hasChanges = true - } - default: - continue - } - } - } - if !updatedPeerPresences.isEmpty { - for i in 0 ..< self.entries.count { - switch self.entries[i] { - case let .MessageEntry(index, message, readState, settings, embeddedState, peer, _, summaryInfo, hasFailed, isContact): - var presencePeerId = peer.peerId - if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId { - presencePeerId = associatedPeerId - } - if let presence = updatedPeerPresences[presencePeerId] { - self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, presence, summaryInfo, hasFailed, isContact) - hasChanges = true - } - default: - continue - } - } - } - if !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageActionsSummaries.isEmpty { - for i in 0 ..< self.entries.count { - switch self.entries[i] { - case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, currentSummary, hasFailed, isContact): - var updatedTagSummaryCount: Int32? - var updatedActionsSummaryCount: Int32? - - if let tagSummary = self.summaryComponents.tagSummary { - let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: index.messageIndex.id.peerId, namespace: tagSummary.namespace) - if let summary = transaction.currentUpdatedMessageTagSummaries[key] { - updatedTagSummaryCount = summary.count - } - } - - if let actionsSummary = self.summaryComponents.actionsSummary { - let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: index.messageIndex.id.peerId, namespace: actionsSummary.namespace) - if let count = transaction.currentUpdatedMessageActionsSummaries[key] { - updatedActionsSummaryCount = count - } - } - - if updatedTagSummaryCount != nil || updatedActionsSummaryCount != nil { - let summaryInfo = ChatListMessageTagSummaryInfo(tagSummaryCount: updatedTagSummaryCount ?? currentSummary.tagSummaryCount, actionsSummaryCount: updatedActionsSummaryCount ?? currentSummary.actionsSummaryCount) - - self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact) - hasChanges = true - } - default: - continue - } - } - } var updateAdditionalItems = false if let itemIds = transaction.replacedAdditionalChatListItems { self.additionalItemIds = Set(itemIds) @@ -803,228 +519,24 @@ final class MutableChatListView { } hasChanges = true - } - return hasChanges - } - - func add(_ initialEntry: MutableChatListEntry, postbox: Postbox) -> Bool { - if let filterPredicate = self.filterPredicate { - switch initialEntry { - case .IntermediateMessageEntry(let index, _, _, _), .MessageEntry(let index, _, _, _, _, _, _, _, _, _): - if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) { - let isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false - let isContact = postbox.contactsTable.isContact(peerId: peer.id) - if !filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread: isUnread, isContact: isContact, isArchived: false) { - return false - } - if self.additionalMixedItemIds.contains(peer.id) { - return false - } - if self.additionalMixedPinnedItemIds.contains(peer.id) { - return false - } - } else { - return false - } - break - default: - break - } - } - - let entry = processedChatListEntry(initialEntry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable) - - if self.entries.count == 0 { - self.entries.append(entry) - return true - } else { - let first = self.entries[self.entries.count - 1] - let last = self.entries[0] - - let next = self.later - - if entry.index < last.index { - if self.earlier == nil || self.earlier!.index < entry.index { - if self.entries.count < self.count { - self.entries.insert(entry, at: 0) - } else { - self.earlier = entry - } - return true - } else { - return false - } - } else if entry.index > first.index { - if next != nil && entry.index > next!.index { - if self.later == nil || self.later!.index > entry.index { - if self.entries.count < self.count { - self.entries.append(entry) - } else { - self.later = entry - } - return true - } else { - return false - } - } else { - self.entries.append(entry) - if self.entries.count > self.count { - self.earlier = self.entries[0] - self.entries.remove(at: 0) - } - return true - } - } else if entry != last && entry != first { - var i = self.entries.count - while i >= 1 { - if self.entries[i - 1].index < entry.index { - break - } - i -= 1 - } - self.entries.insert(entry, at: i) - if self.entries.count > self.count { - self.earlier = self.entries[0] - self.entries.remove(at: 0) - } - return true - } else { - return false - } - } - } - - private func remove(_ indices: Set, type: ChatListEntryType, context: MutableChatListViewReplayContext) -> Bool { - var hasChanges = false - if let earlier = self.earlier, indices.contains(earlier.index) { - var match = false - switch earlier { - case .HoleEntry: - match = type == .hole - case .IntermediateMessageEntry, .MessageEntry: - match = type == .message - /*case .IntermediateGroupReferenceEntry, .GroupReferenceEntry: - match = type == .groupReference*/ - } - if match { - context.invalidEarlier = true - hasChanges = true - } - } - - if let later = self.later, indices.contains(later.index) { - var match = false - switch later { - case .HoleEntry: - match = type == .hole - case .IntermediateMessageEntry, .MessageEntry: - match = type == .message - /*case .IntermediateGroupReferenceEntry, .GroupReferenceEntry: - match = type == .groupReference*/ - } - if match { - context.invalidLater = true - hasChanges = true - } - } - - if self.entries.count != 0 { - var i = self.entries.count - 1 - while i >= 0 { - if indices.contains(self.entries[i].index) { - var match = false - switch self.entries[i] { - case .HoleEntry: - match = type == .hole - case .IntermediateMessageEntry, .MessageEntry: - match = type == .message - /*case .IntermediateGroupReferenceEntry, .GroupReferenceEntry: - match = type == .groupReference*/ - } - if match { - self.entries.remove(at: i) - context.removedEntries = true - hasChanges = true - } - } - i -= 1 - } - } - + }*/ return hasChanges } func complete(postbox: Postbox, context: MutableChatListViewReplayContext) { - if context.removedEntries { - var index = ChatListIndex.absoluteUpperBound - if !self.entries.isEmpty && self.later != nil { - index = self.entries[self.entries.count / 2].index - } - - let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.count, filterPredicate: self.filterPredicate) - var previousEntryByPeerId: [PeerId: MutableChatListEntry] = [:] - for entry in self.entries { - switch entry { - case let .MessageEntry(messageEntry): - previousEntryByPeerId[messageEntry.0.messageIndex.id.peerId] = entry - default: - break - } - } - self.entries = entries - for i in 0 ..< self.entries.count { - switch self.entries[i] { - case let .MessageEntry(messageEntry): - if let previousEntry = previousEntryByPeerId[messageEntry.0.messageIndex.id.peerId] { - self.entries[i] = previousEntry - } - default: - break - } - } - self.earlier = earlier - self.later = later - } else { - if context.invalidEarlier { - var earlyId: ChatListIndex? - let i = 0 - if i < self.entries.count { - earlyId = self.entries[i].index - } - - let earlierEntries = postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlyId, count: 1, filterPredicate: self.filterPredicate) - self.earlier = earlierEntries.first - } - - if context.invalidLater { - var laterId: ChatListIndex? - let i = self.entries.count - 1 - if i >= 0 { - laterId = self.entries[i].index - } - - let laterEntries = postbox.fetchLaterChatEntries(groupId: self.groupId, index: laterId, count: 1, filterPredicate: self.filterPredicate) - self.later = laterEntries.first - } - } + } func firstHole() -> ChatListHole? { - for entry in self.entries { - if case let .HoleEntry(hole) = entry { - return hole - } - } - - return nil + return self.sampledState.hole } - private func renderEntry(_ entry: MutableChatListEntry, postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) -> MutableChatListEntry? { + /*private func renderEntry(_ entry: MutableChatListEntry, postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) -> MutableChatListEntry? { switch entry { - case let .IntermediateMessageEntry(index, message, combinedReadState, embeddedState): + case let .IntermediateMessageEntry(index, messageIndex): let renderedMessage: Message? - if let message = message { - renderedMessage = renderMessage(message) + if let messageIndex = messageIndex { + renderedMessage = postbox.messageHistoryTable.getMessage(messageIndex).flatMap(renderMessage) } else { renderedMessage = nil } @@ -1063,14 +575,14 @@ final class MutableChatListView { actionsSummaryCount = postbox.pendingMessageActionsMetadataTable.getCount(.peerNamespaceAction(key.peerId, key.namespace, key.type)) } - return .MessageEntry(index, renderedMessage, combinedReadState, notificationSettings, embeddedState, RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence, ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount), postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact) + return .MessageEntry(index: index, message: renderedMessage, readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: notificationSettings, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId)?.chatListEmbeddedState, renderedPeer: RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence: presence, tagSummaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount), hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact) default: return nil } - } + }*/ func render(postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) { - for i in 0 ..< self.entries.count { + /*for i in 0 ..< self.entries.count { if let updatedEntry = self.renderEntry(self.entries[i], postbox: postbox, renderMessage: renderMessage, getPeer: getPeer, getPeerNotificationSettings: getPeerNotificationSettings, getPeerPresence: getPeerPresence) { self.entries[i] = updatedEntry } @@ -1089,7 +601,7 @@ final class MutableChatListView { if let updatedEntry = self.renderEntry(self.additionalMixedPinnedEntries[i], postbox: postbox, renderMessage: renderMessage, getPeer: getPeer, getPeerNotificationSettings: getPeerNotificationSettings, getPeerPresence: getPeerPresence) { self.additionalMixedPinnedEntries[i] = updatedEntry } - } + }*/ } } @@ -1105,7 +617,7 @@ public final class ChatListView { self.groupId = mutableView.groupId var entries: [ChatListEntry] = [] - for entry in mutableView.entries { + for entry in mutableView.sampledState.entries { switch entry { case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact): entries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)) @@ -1115,7 +627,7 @@ public final class ChatListView { assertionFailure() } } - if !mutableView.additionalMixedItemEntries.isEmpty || !mutableView.additionalMixedPinnedEntries.isEmpty { + /*if !mutableView.additionalMixedItemEntries.isEmpty || !mutableView.additionalMixedPinnedEntries.isEmpty { var existingIds = Set() for entry in entries { if case let .MessageEntry(messageEntry) = entry { @@ -1161,14 +673,15 @@ public final class ChatListView { } } entries.sort() - } - self.groupEntries = mutableView.groupEntries + }*/ self.entries = entries - self.earlierIndex = mutableView.earlier?.index - self.laterIndex = mutableView.later?.index + self.earlierIndex = mutableView.sampledState.lower?.index + self.laterIndex = mutableView.sampledState.upper?.index + + self.groupEntries = mutableView.groupEntries var additionalItemEntries: [ChatListEntry] = [] - for entry in mutableView.additionalItemEntries { + /*for entry in mutableView.additionalItemEntries { switch entry { case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact): additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)) @@ -1177,7 +690,7 @@ public final class ChatListView { case .IntermediateMessageEntry: assertionFailure() } - } + }*/ self.additionalItemEntries = additionalItemEntries } diff --git a/submodules/Postbox/Sources/ChatListViewState.swift b/submodules/Postbox/Sources/ChatListViewState.swift new file mode 100644 index 0000000000..a9634f8552 --- /dev/null +++ b/submodules/Postbox/Sources/ChatListViewState.swift @@ -0,0 +1,994 @@ + +enum ChatListViewSpacePinned { + case notPinned + case includePinned + case includePinnedAsUnpinned + + var include: Bool { + switch self { + case .notPinned: + return false + case .includePinned, .includePinnedAsUnpinned: + return true + } + } +} + +enum ChatListViewSpace: Hashable { + case group(groupId: PeerGroupId, pinned: ChatListViewSpacePinned) +} + +private func mappedChatListFilterPredicate(postbox: Postbox, groupId: PeerGroupId, predicate: ChatListFilterPredicate) -> (ChatListIntermediateEntry) -> Bool { + return { entry in + switch entry { + case let .message(index, _): + if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) { + let isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false + let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id + let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId) + if predicate.includes(peer: peer, groupId: groupId, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact) { + return true + } else { + return false + } + } else { + return false + } + case .hole: + return true + } + } +} + +private func updateMessagePeers(_ message: Message, updatedPeers: [PeerId: Peer]) -> Message? { + var updated = false + for (peerId, currentPeer) in message.peers { + if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) { + updated = true + break + } + } + if updated { + var peers = SimpleDictionary() + for (peerId, currentPeer) in message.peers { + if let updatedPeer = updatedPeers[peerId] { + peers[peerId] = updatedPeer + } else { + peers[peerId] = currentPeer + } + } + return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds) + } + return nil +} + +private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [PeerId: Peer]) -> RenderedPeer? { + var updated = false + for (peerId, currentPeer) in renderedPeer.peers { + if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) { + updated = true + break + } + } + if updated { + var peers = SimpleDictionary() + for (peerId, currentPeer) in renderedPeer.peers { + if let updatedPeer = updatedPeers[peerId] { + peers[peerId] = updatedPeer + } else { + peers[peerId] = currentPeer + } + } + return RenderedPeer(peerId: renderedPeer.peerId, peers: peers) + } + return nil +} + +private final class ChatListViewSpaceState { + private let space: ChatListViewSpace + private let anchorIndex: MutableChatListEntryIndex + private let filterPredicate: ChatListFilterPredicate? + private let summaryComponents: ChatListEntrySummaryComponents + private let halfLimit: Int + + var orderedEntries: OrderedChatListViewEntries + + init(postbox: Postbox, space: ChatListViewSpace, anchorIndex: MutableChatListEntryIndex, filterPredicate: ChatListFilterPredicate?, summaryComponents: ChatListEntrySummaryComponents, halfLimit: Int) { + self.space = space + self.anchorIndex = anchorIndex + self.filterPredicate = filterPredicate + self.summaryComponents = summaryComponents + self.halfLimit = halfLimit + self.orderedEntries = OrderedChatListViewEntries(lowerOrAtAnchor: [], higherThanAnchor: []) + self.fillSpace(postbox: postbox) + } + + private func fillSpace(postbox: Postbox) { + switch self.space { + case let .group(groupId, pinned): + let lowerBound: MutableChatListEntryIndex + let upperBound: MutableChatListEntryIndex + if pinned.include { + upperBound = .absoluteUpperBound + lowerBound = MutableChatListEntryIndex(index: ChatListIndex.pinnedLowerBound, isMessage: true) + } else { + upperBound = MutableChatListEntryIndex(index: ChatListIndex.pinnedLowerBound.predecessor, isMessage: true) + lowerBound = .absoluteLowerBound + } + let resolvedAnchorIndex = min(upperBound, max(self.anchorIndex, lowerBound)) + + var lowerOrAtAnchorMessages: [MutableChatListEntry] = self.orderedEntries.lowerOrAtAnchor.reversed() + var higherThanAnchorMessages: [MutableChatListEntry] = self.orderedEntries.higherThanAnchor + + func mapEntry(_ entry: ChatListIntermediateEntry) -> MutableChatListEntry { + switch entry { + case let .message(index, messageIndex): + var updatedIndex = index + if case .includePinnedAsUnpinned = pinned { + updatedIndex = ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex) + } + return .IntermediateMessageEntry(index: updatedIndex, messageIndex: messageIndex) + case let .hole(hole): + return .HoleEntry(hole) + } + } + + if case .includePinnedAsUnpinned = pinned { + 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: self.filterPredicate.flatMap { mappedChatListFilterPredicate(postbox: postbox, groupId: groupId, predicate: $0) }).map(mapEntry).sorted(by: { $0.entryIndex < $1.entryIndex }) + + if lowerOrAtAnchorMessages.count < self.halfLimit { + var nextLowerIndex: MutableChatListEntryIndex + if let lastMessage = lowerOrAtAnchorMessages.min(by: { $0.entryIndex < $1.entryIndex }) { + nextLowerIndex = lastMessage.entryIndex.predecessor + } else { + nextLowerIndex = resolvedAnchorIndex + } + var loadedLowerMessages = Array(loadedMessages.filter({ $0.entryIndex <= nextLowerIndex }).reversed()) + let lowerLimit = self.halfLimit - lowerOrAtAnchorMessages.count + if loadedLowerMessages.count > lowerLimit { + loadedLowerMessages.removeLast(loadedLowerMessages.count - lowerLimit) + } + lowerOrAtAnchorMessages.append(contentsOf: loadedLowerMessages) + } + if higherThanAnchorMessages.count < self.halfLimit { + var nextHigherIndex: MutableChatListEntryIndex + if let lastMessage = higherThanAnchorMessages.max(by: { $0.entryIndex < $1.entryIndex }) { + nextHigherIndex = lastMessage.entryIndex.successor + } else { + nextHigherIndex = resolvedAnchorIndex + } + var loadedHigherMessages = loadedMessages.filter({ $0.entryIndex >= nextHigherIndex }) + let higherLimit = self.halfLimit - higherThanAnchorMessages.count + if loadedHigherMessages.count > higherLimit { + loadedHigherMessages.removeLast(loadedHigherMessages.count - higherLimit) + } + higherThanAnchorMessages.append(contentsOf: loadedHigherMessages) + } + } + } else { + if lowerOrAtAnchorMessages.count < self.halfLimit { + var nextLowerIndex: MutableChatListEntryIndex + if let lastMessage = lowerOrAtAnchorMessages.min(by: { $0.entryIndex < $1.entryIndex }) { + nextLowerIndex = lastMessage.entryIndex.predecessor + } else { + nextLowerIndex = resolvedAnchorIndex + } + 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: self.filterPredicate.flatMap { mappedChatListFilterPredicate(postbox: postbox, groupId: groupId, predicate: $0) }).map(mapEntry) + lowerOrAtAnchorMessages.append(contentsOf: loadedLowerMessages) + } + if higherThanAnchorMessages.count < self.halfLimit { + var nextHigherIndex: MutableChatListEntryIndex + if let lastMessage = higherThanAnchorMessages.max(by: { $0.entryIndex < $1.entryIndex }) { + nextHigherIndex = lastMessage.entryIndex.successor + } 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: self.filterPredicate.flatMap { mappedChatListFilterPredicate(postbox: postbox, groupId: groupId, predicate: $0) }).map(mapEntry) + higherThanAnchorMessages.append(contentsOf: loadedHigherMessages) + } + } + + lowerOrAtAnchorMessages.reverse() + + assert(lowerOrAtAnchorMessages.count <= self.halfLimit) + assert(higherThanAnchorMessages.count <= self.halfLimit) + + let allIndices = (lowerOrAtAnchorMessages + higherThanAnchorMessages).map { $0.entryIndex } + assert(Set(allIndices).count == allIndices.count) + assert(allIndices.sorted() == allIndices) + + let entries = OrderedChatListViewEntries(lowerOrAtAnchor: lowerOrAtAnchorMessages, higherThanAnchor: higherThanAnchorMessages) + self.orderedEntries = entries + } + } + + func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool { + var hasUpdates = false + var hadRemovals = false + for (groupId, operations) in transaction.chatListOperations { + let matchesSpace: Bool + switch self.space { + case .group(groupId, _): + matchesSpace = true + default: + matchesSpace = false + } + if !matchesSpace { + continue + } + + inner: for operation in operations { + switch operation { + case let .InsertEntry(index, messageIndex): + switch self.space { + case let .group(_, pinned) where (index.pinningIndex != nil) == pinned.include: + var updatedIndex = index + if case .includePinnedAsUnpinned = pinned { + updatedIndex = ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex) + } + if let filterPredicate = self.filterPredicate { + if let peer = postbox.peerTable.get(updatedIndex.messageIndex.id.peerId) { + let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id + if !filterPredicate.includes(peer: peer, groupId: groupId, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: postbox.readStateTable.getCombinedState(peer.id)?.isUnread ?? false, isContact: postbox.contactsTable.isContact(peerId: notificationsPeerId)) { + continue inner + } + } else { + continue inner + } + } + if self.add(entry: .IntermediateMessageEntry(index: updatedIndex, messageIndex: messageIndex)) { + hasUpdates = true + } + default: + break + } + case let .InsertHole(hole): + switch self.space { + case let .group(_, pinned) where !pinned.include: + if self.add(entry: .HoleEntry(hole)) { + hasUpdates = true + } + default: + break + } + case let .RemoveEntry(indices): + for index in indices { + var updatedIndex = index + if case .group(_, .includePinnedAsUnpinned) = self.space { + updatedIndex = ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex) + } + + if self.orderedEntries.remove(index: MutableChatListEntryIndex(index: updatedIndex, isMessage: true)) { + hasUpdates = true + hadRemovals = true + } + } + case let .RemoveHoles(indices): + for index in indices { + if self.orderedEntries.remove(index: MutableChatListEntryIndex(index: index, isMessage: false)) { + hasUpdates = true + hadRemovals = true + } + } + } + } + } + + if !transaction.currentUpdatedPeerNotificationSettings.isEmpty, let filterPredicate = self.filterPredicate, case let .group(groupId, _) = self.space { + var removeEntryIndices: [MutableChatListEntryIndex] = [] + let _ = self.orderedEntries.mutableScan { entry in + let entryPeer: Peer + let entryNotificationsPeerId: PeerId + switch entry { + case let .MessageEntry(messageEntry): + if let peer = messageEntry.renderedPeer.peer { + entryPeer = peer + entryNotificationsPeerId = peer.notificationSettingsPeerId ?? peer.id + } else { + return nil + } + case let .IntermediateMessageEntry(intermediateMessageEntry): + if let peer = postbox.peerTable.get(intermediateMessageEntry.index.messageIndex.id.peerId) { + entryPeer = peer + entryNotificationsPeerId = peer.notificationSettingsPeerId ?? peer.id + } else { + return nil + } + case .HoleEntry: + return nil + } + if let settingsChange = transaction.currentUpdatedPeerNotificationSettings[entryNotificationsPeerId] { + let isUnread = postbox.readStateTable.getCombinedState(entryPeer.id)?.isUnread ?? false + let wasIncluded = filterPredicate.includes(peer: entryPeer, groupId: groupId, notificationSettings: settingsChange.0, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: entryNotificationsPeerId)) + let isIncluded = filterPredicate.includes(peer: entryPeer, groupId: groupId, notificationSettings: settingsChange.1, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: entryNotificationsPeerId)) + if wasIncluded != isIncluded { + if !isIncluded { + removeEntryIndices.append(entry.entryIndex) + } + } + } + return nil + } + if !removeEntryIndices.isEmpty { + hasUpdates = true + hadRemovals = true + for index in removeEntryIndices { + let _ = self.orderedEntries.remove(index: index) + } + } + for (peerId, settingsChange) in transaction.currentUpdatedPeerNotificationSettings { + if let mainPeer = postbox.peerTable.get(peerId) { + var peers: [Peer] = [mainPeer] + for associatedId in postbox.reverseAssociatedPeerTable.get(peerId: mainPeer.id) { + if let associatedPeer = postbox.peerTable.get(associatedId) { + peers.append(associatedPeer) + } + } + assert(Set(peers.map { $0.id }).count == peers.count) + + let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false + let wasIncluded = filterPredicate.includes(peer: mainPeer, groupId: groupId, notificationSettings: settingsChange.0, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId)) + let isIncluded = filterPredicate.includes(peer: mainPeer, groupId: groupId, notificationSettings: settingsChange.1, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId)) + if wasIncluded != isIncluded { + if isIncluded { + for peer in peers { + let tableEntry: ChatListIntermediateEntry? + tableEntry = postbox.chatListTable.getEntry(peerId: peer.id, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) + if let entry = tableEntry { + switch entry { + case let .message(index, messageIndex): + if self.add(entry: .IntermediateMessageEntry(index: index, messageIndex: messageIndex)) { + hasUpdates = true + } + default: + break + } + } + } + } + } + } + } + } + + if !transaction.currentUpdatedPeerNotificationSettings.isEmpty { + if self.orderedEntries.mutableScan({ entry in + switch entry { + case let .MessageEntry(messageEntry): + if let peer = messageEntry.renderedPeer.peer { + let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id + if let (_, updated) = transaction.currentUpdatedPeerNotificationSettings[notificationsPeerId] { + return .MessageEntry(index: messageEntry.index, message: messageEntry.message, readState: messageEntry.readState, notificationSettings: updated, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: messageEntry.renderedPeer, presence: messageEntry.presence, tagSummaryInfo: messageEntry.tagSummaryInfo, hasFailedMessages: messageEntry.hasFailedMessages, isContact: messageEntry.isContact) + } else { + return nil + } + } else { + return nil + } + default: + return nil + } + }) { + hasUpdates = true + } + } + + if !transaction.updatedFailedMessagePeerIds.isEmpty { + if self.orderedEntries.mutableScan({ entry in + switch entry { + case let .MessageEntry(messageEntry): + if transaction.updatedFailedMessagePeerIds.contains(messageEntry.index.messageIndex.id.peerId) { + return .MessageEntry(index: messageEntry.index, message: messageEntry.message, readState: messageEntry.readState, notificationSettings: messageEntry.notificationSettings, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: messageEntry.renderedPeer, presence: messageEntry.presence, tagSummaryInfo: messageEntry.tagSummaryInfo, hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: messageEntry.index.messageIndex.id.peerId), isContact: messageEntry.isContact) + } else { + return nil + } + default: + return nil + } + }) { + hasUpdates = true + } + } + + if !transaction.currentUpdatedPeers.isEmpty { + if self.orderedEntries.mutableScan({ entry in + switch entry { + case let .MessageEntry(messageEntry): + var updatedMessage: Message? + if let message = messageEntry.message { + updatedMessage = updateMessagePeers(message, updatedPeers: transaction.currentUpdatedPeers) + } + let renderedPeer = updatedRenderedPeer(messageEntry.renderedPeer, updatedPeers: transaction.currentUpdatedPeers) + + if updatedMessage != nil || renderedPeer != nil { + return .MessageEntry(index: messageEntry.index, message: updatedMessage ?? messageEntry.message, readState: messageEntry.readState, notificationSettings: messageEntry.notificationSettings, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: renderedPeer ?? messageEntry.renderedPeer, presence: messageEntry.presence, tagSummaryInfo: messageEntry.tagSummaryInfo, hasFailedMessages: messageEntry.hasFailedMessages, isContact: messageEntry.isContact) + } else { + return nil + } + default: + return nil + } + }) { + hasUpdates = true + } + } + + if !transaction.currentUpdatedPeerPresences.isEmpty { + if self.orderedEntries.mutableScan({ entry in + switch entry { + case let .MessageEntry(messageEntry): + var presencePeerId = messageEntry.renderedPeer.peerId + if let peer = messageEntry.renderedPeer.peers[messageEntry.renderedPeer.peerId], let associatedPeerId = peer.associatedPeerId { + presencePeerId = associatedPeerId + } + if let presence = transaction.currentUpdatedPeerPresences[presencePeerId] { + return .MessageEntry(index: messageEntry.index, message: messageEntry.message, readState: messageEntry.readState, notificationSettings: messageEntry.notificationSettings, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: messageEntry.renderedPeer, presence: presence, tagSummaryInfo: messageEntry.tagSummaryInfo, hasFailedMessages: messageEntry.hasFailedMessages, isContact: messageEntry.isContact) + } else { + return nil + } + default: + return nil + } + }) { + hasUpdates = true + } + } + + if !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageActionsSummaries.isEmpty { + if self.orderedEntries.mutableScan({ entry in + switch entry { + case let .MessageEntry(messageEntry): + var updatedTagSummaryCount: Int32? + var updatedActionsSummaryCount: Int32? + + if let tagSummary = self.summaryComponents.tagSummary { + let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: messageEntry.index.messageIndex.id.peerId, namespace: tagSummary.namespace) + if let summary = transaction.currentUpdatedMessageTagSummaries[key] { + updatedTagSummaryCount = summary.count + } + } + + if let actionsSummary = self.summaryComponents.actionsSummary { + let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: messageEntry.index.messageIndex.id.peerId, namespace: actionsSummary.namespace) + if let count = transaction.currentUpdatedMessageActionsSummaries[key] { + updatedActionsSummaryCount = count + } + } + + if updatedTagSummaryCount != nil || updatedActionsSummaryCount != nil { + let summaryInfo = ChatListMessageTagSummaryInfo(tagSummaryCount: updatedTagSummaryCount ?? messageEntry.tagSummaryInfo.tagSummaryCount, actionsSummaryCount: updatedActionsSummaryCount ?? messageEntry.tagSummaryInfo.actionsSummaryCount) + + return .MessageEntry(index: messageEntry.index, message: messageEntry.message, readState: messageEntry.readState, notificationSettings: messageEntry.notificationSettings, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: messageEntry.renderedPeer, presence: messageEntry.presence, tagSummaryInfo: summaryInfo, hasFailedMessages: messageEntry.hasFailedMessages, isContact: messageEntry.isContact) + } else { + return nil + } + default: + return nil + } + }) { + hasUpdates = true + } + } + + if hadRemovals { + self.fillSpace(postbox: postbox) + } + return hasUpdates + } + + private func add(entry: MutableChatListEntry) -> Bool { + if self.anchorIndex >= entry.entryIndex { + let insertionIndex = binaryInsertionIndex(self.orderedEntries.lowerOrAtAnchor, extract: { $0.entryIndex }, searchItem: entry.entryIndex) + + if insertionIndex < self.orderedEntries.lowerOrAtAnchor.count { + if self.orderedEntries.lowerOrAtAnchor[insertionIndex].entryIndex == entry.entryIndex { + assertionFailure("Inserting an existing index is not allowed") + self.orderedEntries.setLowerOrAtAnchorAtArrayIndex(insertionIndex, to: entry) + return true + } + } + + if insertionIndex == 0 && self.orderedEntries.lowerOrAtAnchor.count >= self.halfLimit { + return false + } + self.orderedEntries.insertLowerOrAtAnchorAtArrayIndex(insertionIndex, value: entry) + if self.orderedEntries.lowerOrAtAnchor.count > self.halfLimit { + self.orderedEntries.removeLowerOrAtAnchorAtArrayIndex(0) + } + return true + } else { + let insertionIndex = binaryInsertionIndex(orderedEntries.higherThanAnchor, extract: { $0.entryIndex }, searchItem: entry.entryIndex) + + if insertionIndex < self.orderedEntries.higherThanAnchor.count { + if self.orderedEntries.higherThanAnchor[insertionIndex].entryIndex == entry.entryIndex { + assertionFailure("Inserting an existing index is not allowed") + self.orderedEntries.setHigherThanAnchorAtArrayIndex(insertionIndex, to: entry) + return true + } + } + + if insertionIndex == self.orderedEntries.higherThanAnchor.count && self.orderedEntries.higherThanAnchor.count >= self.halfLimit { + return false + } + self.orderedEntries.insertHigherThanAnchorAtArrayIndex(insertionIndex, value: entry) + if self.orderedEntries.higherThanAnchor.count > self.halfLimit { + self.orderedEntries.removeHigherThanAnchorAtArrayIndex(self.orderedEntries.higherThanAnchor.count - 1) + } + return true + } + } +} + +private struct MutableChatListEntryIndex: Hashable, Comparable { + var index: ChatListIndex + var isMessage: Bool + + var predecessor: MutableChatListEntryIndex { + return MutableChatListEntryIndex(index: self.index.predecessor, isMessage: true) + } + + var successor: MutableChatListEntryIndex { + return MutableChatListEntryIndex(index: self.index.successor, isMessage: true) + } + + static let absoluteLowerBound = MutableChatListEntryIndex(index: .absoluteLowerBound, isMessage: true) + static let absoluteUpperBound = MutableChatListEntryIndex(index: .absoluteUpperBound, isMessage: true) + + static func <(lhs: MutableChatListEntryIndex, rhs: MutableChatListEntryIndex) -> Bool { + if lhs.index != rhs.index { + return lhs.index < rhs.index + } else if lhs.isMessage != rhs.isMessage { + return lhs.isMessage + } else { + return false + } + } +} + +private extension MutableChatListEntry { + var messagePeerId: PeerId? { + switch self { + case let .IntermediateMessageEntry(intermediateMessageEntry): + return intermediateMessageEntry.0.messageIndex.id.peerId + case let .MessageEntry(messageEntry): + return messageEntry.0.messageIndex.id.peerId + case .HoleEntry: + return nil + } + } + + var entryIndex: MutableChatListEntryIndex { + switch self { + case let .IntermediateMessageEntry(intermediateMessageEntry): + return MutableChatListEntryIndex(index: intermediateMessageEntry.index, isMessage: true) + case let .MessageEntry(messageEntry): + return MutableChatListEntryIndex(index: messageEntry.index, isMessage: true) + case let .HoleEntry(hole): + return MutableChatListEntryIndex(index: ChatListIndex(pinningIndex: nil, messageIndex: hole.index), isMessage: false) + } + } +} + +private struct OrderedChatListViewEntries { + private(set) var lowerOrAtAnchor: [MutableChatListEntry] + private(set) var higherThanAnchor: [MutableChatListEntry] + + private(set) var reverseIndices: [PeerId: [MutableChatListEntryIndex]] = [:] + + fileprivate init(lowerOrAtAnchor: [MutableChatListEntry], higherThanAnchor: [MutableChatListEntry]) { + self.lowerOrAtAnchor = lowerOrAtAnchor + self.higherThanAnchor = higherThanAnchor + + for entry in lowerOrAtAnchor { + if let peerId = entry.messagePeerId { + if self.reverseIndices[peerId] == nil { + self.reverseIndices[peerId] = [entry.entryIndex] + } else { + self.reverseIndices[peerId]!.append(entry.entryIndex) + } + } + } + for entry in higherThanAnchor { + if let peerId = entry.messagePeerId { + if self.reverseIndices[peerId] == nil { + self.reverseIndices[peerId] = [entry.entryIndex] + } else { + self.reverseIndices[peerId]!.append(entry.entryIndex) + } + } + } + } + + mutating func setLowerOrAtAnchorAtArrayIndex(_ index: Int, to value: MutableChatListEntry) { + let previousIndex = self.lowerOrAtAnchor[index].entryIndex + let updatedIndex = value.entryIndex + let previousPeerId = self.lowerOrAtAnchor[index].messagePeerId + let updatedPeerId = value.messagePeerId + + self.lowerOrAtAnchor[index] = value + + if previousPeerId != updatedPeerId { + if let previousPeerId = previousPeerId { + self.reverseIndices[previousPeerId]?.removeAll(where: { $0 == previousIndex }) + if let isEmpty = self.reverseIndices[previousPeerId]?.isEmpty, isEmpty { + self.reverseIndices.removeValue(forKey: previousPeerId) + } + } + if let updatedPeerId = updatedPeerId { + if self.reverseIndices[updatedPeerId] == nil { + self.reverseIndices[updatedPeerId] = [updatedIndex] + } else { + self.reverseIndices[updatedPeerId]!.append(updatedIndex) + } + } + } + } + + mutating func setHigherThanAnchorAtArrayIndex(_ index: Int, to value: MutableChatListEntry) { + let previousIndex = self.higherThanAnchor[index].entryIndex + let updatedIndex = value.entryIndex + let previousPeerId = self.higherThanAnchor[index].messagePeerId + let updatedPeerId = value.messagePeerId + + self.higherThanAnchor[index] = value + + if previousPeerId != updatedPeerId { + if let previousPeerId = previousPeerId { + self.reverseIndices[previousPeerId]?.removeAll(where: { $0 == previousIndex }) + if let isEmpty = self.reverseIndices[previousPeerId]?.isEmpty, isEmpty { + self.reverseIndices.removeValue(forKey: previousPeerId) + } + } + if let updatedPeerId = updatedPeerId { + if self.reverseIndices[updatedPeerId] == nil { + self.reverseIndices[updatedPeerId] = [updatedIndex] + } else { + self.reverseIndices[updatedPeerId]!.append(updatedIndex) + } + } + } + } + + mutating func insertLowerOrAtAnchorAtArrayIndex(_ index: Int, value: MutableChatListEntry) { + self.lowerOrAtAnchor.insert(value, at: index) + + if let peerId = value.messagePeerId { + if self.reverseIndices[peerId] == nil { + self.reverseIndices[peerId] = [value.entryIndex] + } else { + self.reverseIndices[peerId]!.append(value.entryIndex) + } + } + } + + mutating func insertHigherThanAnchorAtArrayIndex(_ index: Int, value: MutableChatListEntry) { + self.higherThanAnchor.insert(value, at: index) + + if let peerId = value.messagePeerId { + if self.reverseIndices[peerId] == nil { + self.reverseIndices[peerId] = [value.entryIndex] + } else { + self.reverseIndices[peerId]!.append(value.entryIndex) + } + } + } + + mutating func removeLowerOrAtAnchorAtArrayIndex(_ index: Int) { + let previousIndex = self.lowerOrAtAnchor[index].entryIndex + if let peerId = self.lowerOrAtAnchor[index].messagePeerId { + self.reverseIndices[peerId]?.removeAll(where: { $0 == previousIndex }) + if let isEmpty = self.reverseIndices[peerId]?.isEmpty, isEmpty { + self.reverseIndices.removeValue(forKey: peerId) + } + } + + self.lowerOrAtAnchor.remove(at: index) + } + + mutating func removeHigherThanAnchorAtArrayIndex(_ index: Int) { + let previousIndex = self.higherThanAnchor[index].entryIndex + if let peerId = self.higherThanAnchor[index].messagePeerId { + self.reverseIndices[peerId]?.removeAll(where: { $0 == previousIndex }) + if let isEmpty = self.reverseIndices[peerId]?.isEmpty, isEmpty { + self.reverseIndices.removeValue(forKey: peerId) + } + } + + self.higherThanAnchor.remove(at: index) + } + + func find(index: MutableChatListEntryIndex) -> MutableChatListEntry? { + if let entryIndex = binarySearch(self.lowerOrAtAnchor, extract: { $0.entryIndex }, searchItem: index) { + return self.lowerOrAtAnchor[entryIndex] + } else if let entryIndex = binarySearch(self.higherThanAnchor, extract: { $0.entryIndex }, searchItem: index) { + return self.higherThanAnchor[entryIndex] + } else { + return nil + } + } + + func indicesForPeerId(_ peerId: PeerId) -> [MutableChatListEntryIndex]? { + return self.reverseIndices[peerId] + } + + var first: MutableChatListEntry? { + return self.lowerOrAtAnchor.first ?? self.higherThanAnchor.first + } + + mutating func mutableScan(_ f: (MutableChatListEntry) -> MutableChatListEntry?) -> Bool { + var anyUpdated = false + for i in 0 ..< self.lowerOrAtAnchor.count { + if let updated = f(self.lowerOrAtAnchor[i]) { + self.setLowerOrAtAnchorAtArrayIndex(i, to: updated) + anyUpdated = true + } + } + for i in 0 ..< self.higherThanAnchor.count { + if let updated = f(self.higherThanAnchor[i]) { + self.setHigherThanAnchorAtArrayIndex(i, to: updated) + anyUpdated = true + } + } + return anyUpdated + } + + mutating func update(index: MutableChatListEntryIndex, _ f: (MutableChatListEntry) -> MutableChatListEntry?) -> Bool { + if let entryIndex = binarySearch(self.lowerOrAtAnchor, extract: { $0.entryIndex }, searchItem: index) { + if let updated = f(self.lowerOrAtAnchor[entryIndex]) { + self.setLowerOrAtAnchorAtArrayIndex(entryIndex, to: updated) + return true + } + } else if let entryIndex = binarySearch(self.higherThanAnchor, extract: { $0.entryIndex }, searchItem: index) { + if let updated = f(self.higherThanAnchor[entryIndex]) { + self.setHigherThanAnchorAtArrayIndex(entryIndex, to: updated) + return true + } + } + return false + } + + mutating func remove(index: MutableChatListEntryIndex) -> Bool { + if let entryIndex = binarySearch(self.lowerOrAtAnchor, extract: { $0.entryIndex }, searchItem: index) { + self.removeLowerOrAtAnchorAtArrayIndex(entryIndex) + return true + } else if let entryIndex = binarySearch(self.higherThanAnchor, extract: { $0.entryIndex }, searchItem: index) { + self.removeHigherThanAnchorAtArrayIndex(entryIndex) + return true + } else { + return false + } + } +} + +final class ChatListViewSample { + let entries: [MutableChatListEntry] + let lower: MutableChatListEntry? + let upper: MutableChatListEntry? + let anchorIndex: ChatListIndex + let hole: ChatListHole? + + fileprivate init(entries: [MutableChatListEntry], lower: MutableChatListEntry?, upper: MutableChatListEntry?, anchorIndex: ChatListIndex, hole: ChatListHole?) { + self.entries = entries + self.lower = lower + self.upper = upper + self.anchorIndex = anchorIndex + self.hole = hole + } +} + +struct ChatListViewState { + private let anchorIndex: MutableChatListEntryIndex + private let filterPredicate: ChatListFilterPredicate? + private let summaryComponents: ChatListEntrySummaryComponents + private let halfLimit: Int + private var stateBySpace: [ChatListViewSpace: ChatListViewSpaceState] = [:] + + init(postbox: Postbox, spaces: [ChatListViewSpace], anchorIndex: ChatListIndex, filterPredicate: ChatListFilterPredicate?, summaryComponents: ChatListEntrySummaryComponents, halfLimit: Int) { + self.anchorIndex = MutableChatListEntryIndex(index: anchorIndex, isMessage: true) + self.filterPredicate = filterPredicate + self.summaryComponents = summaryComponents + self.halfLimit = halfLimit + + for space in spaces { + self.stateBySpace[space] = ChatListViewSpaceState(postbox: postbox, space: space, anchorIndex: self.anchorIndex, filterPredicate: self.filterPredicate, summaryComponents: summaryComponents, halfLimit: halfLimit) + } + } + + func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool { + var updated = false + for (_, state) in self.stateBySpace { + if state.replay(postbox: postbox, transaction: transaction) { + updated = true + } + } + return updated + } + + private func sampleIndices() -> (lowerOrAtAnchor: [(ChatListViewSpace, Int)], higherThanAnchor: [(ChatListViewSpace, Int)]) { + var previousAnchorIndices: [ChatListViewSpace: Int] = [:] + var nextAnchorIndices: [ChatListViewSpace: Int] = [:] + for (space, state) in self.stateBySpace { + previousAnchorIndices[space] = state.orderedEntries.lowerOrAtAnchor.count - 1 + nextAnchorIndices[space] = 0 + } + + var backwardsResult: [(ChatListViewSpace, Int)] = [] + var result: [(ChatListViewSpace, Int)] = [] + + while true { + var minSpace: ChatListViewSpace? + for (space, value) in previousAnchorIndices { + if value != -1 { + if let minSpaceValue = minSpace { + if self.stateBySpace[space]!.orderedEntries.lowerOrAtAnchor[value].entryIndex > self.stateBySpace[minSpaceValue]!.orderedEntries.lowerOrAtAnchor[previousAnchorIndices[minSpaceValue]!].entryIndex { + minSpace = space + } + } else { + minSpace = space + } + } + } + if let minSpace = minSpace { + backwardsResult.append((minSpace, previousAnchorIndices[minSpace]!)) + previousAnchorIndices[minSpace]! -= 1 + if backwardsResult.count == self.halfLimit { + break + } + } + + if minSpace == nil { + break + } + } + + while true { + var maxSpace: ChatListViewSpace? + for (space, value) in nextAnchorIndices { + if value != self.stateBySpace[space]!.orderedEntries.higherThanAnchor.count { + if let maxSpaceValue = maxSpace { + if self.stateBySpace[space]!.orderedEntries.higherThanAnchor[value].entryIndex < self.stateBySpace[maxSpaceValue]!.orderedEntries.higherThanAnchor[nextAnchorIndices[maxSpaceValue]!].entryIndex { + maxSpace = space + } + } else { + maxSpace = space + } + } + } + if let maxSpace = maxSpace { + result.append((maxSpace, nextAnchorIndices[maxSpace]!)) + nextAnchorIndices[maxSpace]! += 1 + if result.count == self.halfLimit { + break + } + } + + if maxSpace == nil { + break + } + } + return (backwardsResult.reversed(), result) + } + + func sample(postbox: Postbox) -> ChatListViewSample { + let combinedSpacesAndIndicesByDirection = self.sampleIndices() + + var result: [MutableChatListEntry] = [] + + var sampledHoleIndices: [Int] = [] + var sampledAnchorBoundaryIndex: Int? + + let directions = [combinedSpacesAndIndicesByDirection.lowerOrAtAnchor, combinedSpacesAndIndicesByDirection.higherThanAnchor] + for directionIndex in 0 ..< directions.count { + outer: for i in 0 ..< directions[directionIndex].count { + let (space, listIndex) = directions[directionIndex][i] + + let entry: MutableChatListEntry + if directionIndex == 0 { + entry = self.stateBySpace[space]!.orderedEntries.lowerOrAtAnchor[listIndex] + } else { + entry = self.stateBySpace[space]!.orderedEntries.higherThanAnchor[listIndex] + } + + if entry.entryIndex >= self.anchorIndex { + sampledAnchorBoundaryIndex = result.count + } + + switch entry { + case let .IntermediateMessageEntry(index, messageIndex): + var peers = SimpleDictionary() + var notificationsPeerId = index.messageIndex.id.peerId + if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) { + peers[peer.id] = peer + if let notificationSettingsPeerId = peer.notificationSettingsPeerId { + notificationsPeerId = notificationSettingsPeerId + } + if let associatedPeerId = peer.associatedPeerId { + if let associatedPeer = postbox.peerTable.get(associatedPeerId) { + peers[associatedPeer.id] = associatedPeer + } + } + } + let renderedPeer = RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers) + + var tagSummaryCount: Int32? + var actionsSummaryCount: Int32? + + if let tagSummary = self.summaryComponents.tagSummary { + let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: index.messageIndex.id.peerId, namespace: tagSummary.namespace) + if let summary = postbox.messageHistoryTagsSummaryTable.get(key) { + tagSummaryCount = summary.count + } + } + + if let actionsSummary = self.summaryComponents.actionsSummary { + let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: index.messageIndex.id.peerId, namespace: actionsSummary.namespace) + actionsSummaryCount = postbox.pendingMessageActionsMetadataTable.getCount(.peerNamespaceAction(key.peerId, key.namespace, key.type)) + } + + let tagSummaryInfo = ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount) + + let updatedEntry: MutableChatListEntry = .MessageEntry(index: index, message: messageIndex.flatMap(postbox.messageHistoryTable.getMessage).flatMap(postbox.renderIntermediateMessage), readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId)?.chatListEmbeddedState, renderedPeer: renderedPeer, presence: postbox.peerPresenceTable.get(index.messageIndex.id.peerId), tagSummaryInfo: tagSummaryInfo, hasFailedMessages: false, isContact: postbox.contactsTable.isContact(peerId: index.messageIndex.id.peerId)) + if directionIndex == 0 { + self.stateBySpace[space]!.orderedEntries.setLowerOrAtAnchorAtArrayIndex(listIndex, to: updatedEntry) + } else { + self.stateBySpace[space]!.orderedEntries.setHigherThanAnchorAtArrayIndex(listIndex, to: updatedEntry) + } + result.append(updatedEntry) + case .MessageEntry: + result.append(entry) + case .HoleEntry: + sampledHoleIndices.append(result.count) + + result.append(entry) + } + } + } + + let allIndices = result.map { $0.entryIndex } + assert(Set(allIndices).count == allIndices.count) + assert(allIndices.sorted() == allIndices) + + var sampledHoleIndex: Int? + if !sampledHoleIndices.isEmpty { + if let sampledAnchorBoundaryIndex = sampledAnchorBoundaryIndex { + var found = false + for i in 0 ..< sampledHoleIndices.count { + if i >= sampledAnchorBoundaryIndex { + sampledHoleIndex = sampledHoleIndices[i] + found = true + break + } + } + if !found { + sampledHoleIndex = sampledHoleIndices.first + } + } else if let index = sampledHoleIndices.first { + sampledHoleIndex = index + } + } + + var sampledHole: ChatListHole? + if let index = sampledHoleIndex { + if case let .HoleEntry(hole) = result[index] { + sampledHole = hole + } else { + assertionFailure() + } + } + + var lower: MutableChatListEntry? + if combinedSpacesAndIndicesByDirection.lowerOrAtAnchor.count >= self.halfLimit { + lower = result[0] + result.removeFirst() + } + + var upper: MutableChatListEntry? + if combinedSpacesAndIndicesByDirection.higherThanAnchor.count >= self.halfLimit { + upper = result.last + result.removeLast() + } + + return ChatListViewSample(entries: result, lower: lower, upper: upper, anchorIndex: self.anchorIndex.index, hole: sampledHole) + } +} diff --git a/submodules/Postbox/Sources/Message.swift b/submodules/Postbox/Sources/Message.swift index 3968afde45..8e73e9b516 100644 --- a/submodules/Postbox/Sources/Message.swift +++ b/submodules/Postbox/Sources/Message.swift @@ -184,10 +184,6 @@ public struct ChatListIndex: Comparable, Hashable { return lhs.messageIndex < rhs.messageIndex } - public var hashValue: Int { - return self.messageIndex.hashValue - } - public static var absoluteUpperBound: ChatListIndex { return ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex.absoluteUpperBound()) } @@ -196,6 +192,10 @@ public struct ChatListIndex: Comparable, Hashable { return ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex.absoluteLowerBound()) } + public static var pinnedLowerBound: ChatListIndex { + return ChatListIndex(pinningIndex: UInt16(Int8.max - 1), messageIndex: MessageIndex.absoluteLowerBound()) + } + public var predecessor: ChatListIndex { return ChatListIndex(pinningIndex: self.pinningIndex, messageIndex: self.messageIndex.predecessor()) } diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index f0fb7ca809..cfdda41f95 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -625,7 +625,7 @@ final class MessageHistoryTable: Table { return messageIds } - func topMessage(_ peerId: PeerId) -> IntermediateMessage? { + func topIndex(peerId: PeerId) -> MessageIndex? { var topIndex: MessageIndex? for namespace in self.messageHistoryIndexTable.existingNamespaces(peerId: peerId) where self.seedConfiguration.chatMessagesNamespaces.contains(namespace) { self.valueBox.range(self.table, start: self.upperBound(peerId: peerId, namespace: namespace), end: self.lowerBound(peerId: peerId, namespace: namespace), keys: { key in @@ -641,7 +641,11 @@ final class MessageHistoryTable: Table { }, limit: 1) } - return topIndex.flatMap(self.getMessage) + return topIndex + } + + func topMessage(peerId: PeerId) -> IntermediateMessage? { + return self.topIndex(peerId: peerId).flatMap(self.getMessage) } func exists(index: MessageIndex) -> Bool { diff --git a/submodules/Postbox/Sources/MessageHistoryViewState.swift b/submodules/Postbox/Sources/MessageHistoryViewState.swift index 731a0d4263..7f46f97daa 100644 --- a/submodules/Postbox/Sources/MessageHistoryViewState.swift +++ b/submodules/Postbox/Sources/MessageHistoryViewState.swift @@ -177,7 +177,7 @@ private func binaryIndexOrLower(_ inputArr: [MutableMessageHistoryEntry], _ sear return hi } -private func sampleEntries(orderedEntriesBySpace: [PeerIdAndNamespace: OrderedHistoryViewEntries], anchor: HistoryViewAnchor, halfLimit: Int) -> (lowerOrAtAnchor:[(PeerIdAndNamespace, Int)], higherThanAnchor: [(PeerIdAndNamespace, Int)]) { +private func sampleEntries(orderedEntriesBySpace: [PeerIdAndNamespace: OrderedHistoryViewEntries], anchor: HistoryViewAnchor, halfLimit: Int) -> (lowerOrAtAnchor: [(PeerIdAndNamespace, Int)], higherThanAnchor: [(PeerIdAndNamespace, Int)]) { var previousAnchorIndices: [PeerIdAndNamespace: Int] = [:] var nextAnchorIndices: [PeerIdAndNamespace: Int] = [:] for (space, items) in orderedEntriesBySpace { @@ -1051,7 +1051,7 @@ final class HistoryViewLoadedState { if let associatedIndices = self.orderedEntriesBySpace[space]!.indicesForAssociatedMessageId(entry.index.id) { for associatedIndex in associatedIndices { - self.orderedEntriesBySpace[space]!.update(index: associatedIndex, { current in + let _ = self.orderedEntriesBySpace[space]!.update(index: associatedIndex, { current in switch current { case .IntermediateMessageEntry: return current diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index abaec43a0a..0b7c6f2dab 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -772,36 +772,28 @@ public final class Transaction { for peerId in predicate.includePeerIds { includedPeerIds[peerId] = false } - var count = postbox.chatListTable.countWithPredicate(groupId: .root, predicate: { peerId in - if let peer = postbox.peerTable.get(peerId) { - let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false - let notificationsPeerId = peer.notificationSettingsPeerId ?? peerId - let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId) - if predicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact, isArchived: false) { - includedPeerIds[peer.id] = true - return true + + var count = 0 + + var groupIds: [PeerGroupId] = [.root] + groupIds.append(contentsOf: predicate.includeAdditionalPeerGroupIds) + for groupId in groupIds { + count += postbox.chatListTable.countWithPredicate(groupId: groupId, predicate: { peerId in + if let peer = postbox.peerTable.get(peerId) { + let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false + let notificationsPeerId = peer.notificationSettingsPeerId ?? peerId + let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId) + if predicate.includes(peer: peer, groupId: groupId, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact) { + includedPeerIds[peer.id] = true + return true + } else { + return false + } } else { return false } - } else { - return false - } - }) - let archivedCount = postbox.chatListTable.countWithPredicate(groupId: .group(1), predicate: { peerId in - if let peer = postbox.peerTable.get(peerId) { - let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false - let notificationsPeerId = peer.notificationSettingsPeerId ?? peerId - let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId) - if predicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact, isArchived: false) { - includedPeerIds[peer.id] = true - return true - } else { - return false - } - } else { - return false - } - }) + }) + } for (peerId, included) in includedPeerIds { if !included { if postbox.chatListTable.getPeerChatListIndex(peerId: peerId) != nil { @@ -809,7 +801,7 @@ public final class Transaction { } } } - return count + archivedCount + return count } public func legacyGetAccessChallengeData() -> PostboxAccessChallengeData { @@ -1647,7 +1639,7 @@ public final class Postbox { switch peerIds { case let .associated(_, messageId): if let messageId = messageId, let readState = self.readStateTable.getCombinedState(messageId.peerId), readState.count != 0 { - if let topMessage = self.messageHistoryTable.topMessage(messageId.peerId) { + if let topMessage = self.messageHistoryTable.topMessage(peerId: messageId.peerId) { let _ = self.messageHistoryTable.applyInteractiveMaxReadIndex(postbox: self, messageIndex: topMessage.index, operationsByPeerId: &self.currentOperationsByPeerId, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations) } } @@ -1702,71 +1694,6 @@ public final class Postbox { self.synchronizeGroupMessageStatsTable.set(groupId: groupId, namespace: namespace, needsValidation: false, operations: &self.currentUpdatedGroupSummarySynchronizeOperations) } - private func mappedChatListFilterPredicate(_ predicate: ChatListFilterPredicate, isArchived: Bool) -> (ChatListIntermediateEntry) -> Bool { - return { entry in - switch entry { - case let .message(index, _, _): - if index.pinningIndex != nil { - return false - } - if let peer = self.peerTable.get(index.messageIndex.id.peerId) { - let isUnread = self.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false - let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id - let isContact = self.contactsTable.isContact(peerId: notificationsPeerId) - if predicate.includes(peer: peer, notificationSettings: self.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact, isArchived: isArchived) { - return true - } else { - return false - } - } else { - return false - } - case .hole: - return true - } - } - } - - func fetchAroundChatEntries(groupId: PeerGroupId, index: ChatListIndex, count: Int, filterPredicate: ChatListFilterPredicate?) -> (entries: [MutableChatListEntry], earlier: MutableChatListEntry?, later: MutableChatListEntry?) { - let mappedPredicate = filterPredicate.flatMap { predicate in - self.mappedChatListFilterPredicate(predicate, isArchived: groupId != .root) - } - let (intermediateEntries, intermediateLower, intermediateUpper) = self.chatListTable.entriesAround(groupId: groupId, index: index, messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate) - let entries: [MutableChatListEntry] = intermediateEntries.map { entry in - return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable) - } - let lower: MutableChatListEntry? = intermediateLower.flatMap { entry in - return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable) - } - let upper: MutableChatListEntry? = intermediateUpper.flatMap { entry in - return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable) - } - - return (entries, lower, upper) - } - - func fetchEarlierChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ChatListFilterPredicate?) -> [MutableChatListEntry] { - let mappedPredicate = filterPredicate.flatMap { predicate in - self.mappedChatListFilterPredicate(predicate, isArchived: groupId != .root) - } - let intermediateEntries = self.chatListTable.earlierEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate) - let entries: [MutableChatListEntry] = intermediateEntries.map { entry in - return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable) - } - return entries - } - - func fetchLaterChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ChatListFilterPredicate?) -> [MutableChatListEntry] { - let mappedPredicate = filterPredicate.flatMap { predicate in - self.mappedChatListFilterPredicate(predicate, isArchived: groupId != .root) - } - let intermediateEntries = self.chatListTable.laterEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate) - let entries: [MutableChatListEntry] = intermediateEntries.map { entry in - return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable) - } - return entries - } - func renderIntermediateMessage(_ message: IntermediateMessage) -> Message { let renderedMessage = self.messageHistoryTable.renderMessage(message, peerTable: self.peerTable) diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index da3048f7e2..95b4c6a601 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -1517,14 +1517,18 @@ public final class SqliteValueBox: ValueBox { switch result { case .accept: acceptedCount += 1 - return true + if limit > 0 && acceptedCount >= limit { + hadStop = true + return false + } else { + return true + } case .skip: return true case .stop: hadStop = true return false } - return true }, limit: limit) if let lastKey = lastKey { currentStart = lastKey diff --git a/submodules/TelegramCore/Sources/ChatListFiltering.swift b/submodules/TelegramCore/Sources/ChatListFiltering.swift index 0eaef22ef0..e3ca7635fd 100644 --- a/submodules/TelegramCore/Sources/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/ChatListFiltering.swift @@ -26,16 +26,14 @@ public struct ChatListFilterPeerCategories: OptionSet, Hashable { public static let contacts = ChatListFilterPeerCategories(rawValue: 1 << 0) public static let nonContacts = ChatListFilterPeerCategories(rawValue: 1 << 1) - public static let smallGroups = ChatListFilterPeerCategories(rawValue: 1 << 2) - public static let largeGroups = ChatListFilterPeerCategories(rawValue: 1 << 3) - public static let channels = ChatListFilterPeerCategories(rawValue: 1 << 4) - public static let bots = ChatListFilterPeerCategories(rawValue: 1 << 5) + public static let groups = ChatListFilterPeerCategories(rawValue: 1 << 2) + public static let channels = ChatListFilterPeerCategories(rawValue: 1 << 3) + public static let bots = ChatListFilterPeerCategories(rawValue: 1 << 4) public static let all: ChatListFilterPeerCategories = [ .contacts, .nonContacts, - .smallGroups, - .largeGroups, + .groups, .channels, .bots ] @@ -50,10 +48,9 @@ private struct ChatListFilterPeerApiCategories: OptionSet { static let contacts = ChatListFilterPeerApiCategories(rawValue: 1 << 0) static let nonContacts = ChatListFilterPeerApiCategories(rawValue: 1 << 1) - static let smallGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 2) - static let largeGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 3) - static let channels = ChatListFilterPeerApiCategories(rawValue: 1 << 4) - static let bots = ChatListFilterPeerApiCategories(rawValue: 1 << 5) + static let groups = ChatListFilterPeerApiCategories(rawValue: 1 << 2) + static let channels = ChatListFilterPeerApiCategories(rawValue: 1 << 3) + static let bots = ChatListFilterPeerApiCategories(rawValue: 1 << 4) } extension ChatListFilterPeerCategories { @@ -66,11 +63,8 @@ extension ChatListFilterPeerCategories { if flags.contains(.nonContacts) { result.insert(.nonContacts) } - if flags.contains(.smallGroups) { - result.insert(.smallGroups) - } - if flags.contains(.largeGroups) { - result.insert(.largeGroups) + if flags.contains(.groups) { + result.insert(.groups) } if flags.contains(.channels) { result.insert(.channels) @@ -89,11 +83,8 @@ extension ChatListFilterPeerCategories { if self.contains(.nonContacts) { result.insert(.nonContacts) } - if self.contains(.smallGroups) { - result.insert(.smallGroups) - } - if self.contains(.largeGroups) { - result.insert(.largeGroups) + if self.contains(.groups) { + result.insert(.groups) } if self.contains(.channels) { result.insert(.channels) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 6304a86943..1a3977d678 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -256,7 +256,9 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati segmentedBackgroundColor: UIColor(rgb: 0x3a3b3d), segmentedForegroundColor: UIColor(rgb: 0x6f7075), segmentedTextColor: UIColor(rgb: 0xffffff), - segmentedDividerColor: UIColor(rgb: 0x505155) + segmentedDividerColor: UIColor(rgb: 0x505155), + clearButtonBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), + clearButtonForegroundColor: UIColor(rgb: 0xffffff) ) let navigationSearchBar = PresentationThemeNavigationSearchBar( diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index f3c6970e6a..29881405f1 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -503,7 +503,9 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres segmentedBackgroundColor: mainInputColor, segmentedForegroundColor: mainBackgroundColor, segmentedTextColor: UIColor(rgb: 0xffffff), - segmentedDividerColor: mainSecondaryTextColor.withAlphaComponent(0.5) + segmentedDividerColor: mainSecondaryTextColor.withAlphaComponent(0.5), + clearButtonBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), + clearButtonForegroundColor: UIColor(rgb: 0xffffff) ) let navigationSearchBar = PresentationThemeNavigationSearchBar( diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 0522f7a94b..20fbe6f575 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -360,7 +360,9 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio segmentedBackgroundColor: UIColor(rgb: 0xe9e9e9), segmentedForegroundColor: UIColor(rgb: 0xf7f7f7), segmentedTextColor: UIColor(rgb: 0x000000), - segmentedDividerColor: UIColor(rgb: 0xd6d6dc) + segmentedDividerColor: UIColor(rgb: 0xd6d6dc), + clearButtonBackgroundColor: UIColor(rgb: 0xE3E3E3, alpha: 0.78), + clearButtonForegroundColor: UIColor(rgb: 0x7f7f7f) ) let navigationSearchBar = PresentationThemeNavigationSearchBar( diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index 003c2d1246..f6e2d4d650 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -126,8 +126,10 @@ public final class PresentationThemeRootNavigationBar { public let segmentedForegroundColor: UIColor public let segmentedTextColor: UIColor public let segmentedDividerColor: UIColor + public let clearButtonBackgroundColor: UIColor + public let clearButtonForegroundColor: UIColor - public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, segmentedBackgroundColor: UIColor, segmentedForegroundColor: UIColor, segmentedTextColor: UIColor, segmentedDividerColor: UIColor) { + public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, segmentedBackgroundColor: UIColor, segmentedForegroundColor: UIColor, segmentedTextColor: UIColor, segmentedDividerColor: UIColor, clearButtonBackgroundColor: UIColor, clearButtonForegroundColor: UIColor) { self.buttonColor = buttonColor self.disabledButtonColor = disabledButtonColor self.primaryTextColor = primaryTextColor @@ -143,10 +145,14 @@ public final class PresentationThemeRootNavigationBar { self.segmentedForegroundColor = segmentedForegroundColor self.segmentedTextColor = segmentedTextColor self.segmentedDividerColor = segmentedDividerColor + self.clearButtonBackgroundColor = clearButtonBackgroundColor + self.clearButtonForegroundColor = clearButtonForegroundColor } - public func withUpdated(buttonColor: UIColor? = nil, disabledButtonColor: UIColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, controlColor: UIColor? = nil, accentTextColor: UIColor? = nil, backgroundColor: UIColor? = nil, separatorColor: UIColor? = nil, badgeBackgroundColor: UIColor? = nil, badgeStrokeColor: UIColor? = nil, badgeTextColor: UIColor? = nil, segmentedBackgroundColor: UIColor? = nil, segmentedForegroundColor: UIColor? = nil, segmentedTextColor: UIColor? = nil, segmentedDividerColor: UIColor? = nil) -> PresentationThemeRootNavigationBar { - return PresentationThemeRootNavigationBar(buttonColor: buttonColor ?? self.buttonColor, disabledButtonColor: disabledButtonColor ?? self.disabledButtonColor, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, controlColor: controlColor ?? self.controlColor, accentTextColor: accentTextColor ?? self.accentTextColor, backgroundColor: backgroundColor ?? self.backgroundColor, separatorColor: separatorColor ?? self.separatorColor, badgeBackgroundColor: badgeBackgroundColor ?? self.badgeBackgroundColor, badgeStrokeColor: badgeStrokeColor ?? self.badgeStrokeColor, badgeTextColor: badgeTextColor ?? self.badgeTextColor, segmentedBackgroundColor: segmentedBackgroundColor ?? self.segmentedBackgroundColor, segmentedForegroundColor: segmentedForegroundColor ?? self.segmentedForegroundColor, segmentedTextColor: segmentedTextColor ?? self.segmentedTextColor, segmentedDividerColor: segmentedDividerColor ?? self.segmentedDividerColor) + public func withUpdated(buttonColor: UIColor? = nil, disabledButtonColor: UIColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, controlColor: UIColor? = nil, accentTextColor: UIColor? = nil, backgroundColor: UIColor? = nil, separatorColor: UIColor? = nil, badgeBackgroundColor: UIColor? = nil, badgeStrokeColor: UIColor? = nil, badgeTextColor: UIColor? = nil, segmentedBackgroundColor: UIColor? = nil, segmentedForegroundColor: UIColor? = nil, segmentedTextColor: UIColor? = nil, segmentedDividerColor: UIColor? = nil, clearButtonBackgroundColor: UIColor? = nil, clearButtonForegroundColor: UIColor? = nil) -> PresentationThemeRootNavigationBar { + let resolvedClearButtonBackgroundColor = clearButtonBackgroundColor ?? self.clearButtonBackgroundColor + let resolvedClearButtonForegroundColor = clearButtonForegroundColor ?? self.clearButtonForegroundColor + return PresentationThemeRootNavigationBar(buttonColor: buttonColor ?? self.buttonColor, disabledButtonColor: disabledButtonColor ?? self.disabledButtonColor, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, controlColor: controlColor ?? self.controlColor, accentTextColor: accentTextColor ?? self.accentTextColor, backgroundColor: backgroundColor ?? self.backgroundColor, separatorColor: separatorColor ?? self.separatorColor, badgeBackgroundColor: badgeBackgroundColor ?? self.badgeBackgroundColor, badgeStrokeColor: badgeStrokeColor ?? self.badgeStrokeColor, badgeTextColor: badgeTextColor ?? self.badgeTextColor, segmentedBackgroundColor: segmentedBackgroundColor ?? self.segmentedBackgroundColor, segmentedForegroundColor: segmentedForegroundColor ?? self.segmentedForegroundColor, segmentedTextColor: segmentedTextColor ?? self.segmentedTextColor, segmentedDividerColor: segmentedDividerColor ?? self.segmentedDividerColor, clearButtonBackgroundColor: resolvedClearButtonBackgroundColor, clearButtonForegroundColor: resolvedClearButtonForegroundColor) } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index 1eef46a36d..857d8b5171 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -402,26 +402,32 @@ extension PresentationThemeRootNavigationBar: Codable { case segmentedFg case segmentedText case segmentedDivider + case clearButtonBackground + case clearButtonForeground } public convenience init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - self.init(buttonColor: try decodeColor(values, .button), - disabledButtonColor: try decodeColor(values, .disabledButton), - primaryTextColor: try decodeColor(values, .primaryText), - secondaryTextColor: try decodeColor(values, .secondaryText), - controlColor: try decodeColor(values, .control), - accentTextColor: try decodeColor(values, .accentText), - backgroundColor: try decodeColor(values, .background), - separatorColor: try decodeColor(values, .separator), - badgeBackgroundColor: try decodeColor(values, .badgeFill), - badgeStrokeColor: try decodeColor(values, .badgeStroke), - badgeTextColor: try decodeColor(values, .badgeText), - segmentedBackgroundColor: try decodeColor(values, .segmentedBg, decoder: decoder, fallbackKey: "root.searchBar.inputFill"), - segmentedForegroundColor: try decodeColor(values, .segmentedFg, decoder: decoder, fallbackKey: "root.navBar.background"), - segmentedTextColor: try decodeColor(values, .segmentedText, decoder: decoder, fallbackKey: "root.navBar.primaryText"), - segmentedDividerColor: try decodeColor(values, .segmentedDivider, decoder: decoder, fallbackKey: "root.list.freeInputField.stroke")) + self.init( + buttonColor: try decodeColor(values, .button), + disabledButtonColor: try decodeColor(values, .disabledButton), + primaryTextColor: try decodeColor(values, .primaryText), + secondaryTextColor: try decodeColor(values, .secondaryText), + controlColor: try decodeColor(values, .control), + accentTextColor: try decodeColor(values, .accentText), + backgroundColor: try decodeColor(values, .background), + separatorColor: try decodeColor(values, .separator), + badgeBackgroundColor: try decodeColor(values, .badgeFill), + badgeStrokeColor: try decodeColor(values, .badgeStroke), + badgeTextColor: try decodeColor(values, .badgeText), + segmentedBackgroundColor: try decodeColor(values, .segmentedBg, decoder: decoder, fallbackKey: "root.searchBar.inputFill"), + segmentedForegroundColor: try decodeColor(values, .segmentedFg, decoder: decoder, fallbackKey: "root.navBar.background"), + segmentedTextColor: try decodeColor(values, .segmentedText, decoder: decoder, fallbackKey: "root.navBar.primaryText"), + segmentedDividerColor: try decodeColor(values, .segmentedDivider, decoder: decoder, fallbackKey: "root.list.freeInputField.stroke"), + clearButtonBackgroundColor: try decodeColor(values, .clearButtonBackground, decoder: decoder, fallbackKey: "root.list.freeInputField.bg"), + clearButtonForegroundColor: try decodeColor(values, .clearButtonForeground, decoder: decoder, fallbackKey: "root.list.freeInputField.primary") + ) } public func encode(to encoder: Encoder) throws { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/Contents.json new file mode 100644 index 0000000000..b82e6802c1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_reorder.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/ic_reorder.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/ic_reorder.pdf new file mode 100644 index 0000000000..b409b12f05 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/ic_reorder.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/ChatListNewFolder.tgs b/submodules/TelegramUI/Resources/Animations/ChatListNewFolder.tgs new file mode 100644 index 0000000000..1a60342622 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/ChatListNewFolder.tgs differ diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index dfa92f1c0b..067b1a4570 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -61,7 +61,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { let sideInset: CGFloat = 13.0 let bottomInset: CGFloat = 10.0 let contentInset: CGFloat = 9.0 - let contentVerticalInset: CGFloat = 9.0 + let contentVerticalInset: CGFloat = 11.0 let animationSize = CGSize(width: 32.0, height: 32.0) let animationInset: CGFloat = (70 - animationSize.width) / 2.0 let animationSpacing: CGFloat = 8.0