From 566816d3732201a9ae12e3c379a4b15a4c2c7a47 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 10 Mar 2020 17:54:16 +0530 Subject: [PATCH] Chat folder improvements --- .../AsyncDisplayKit/Source/ASDisplayNode.mm | 15 + .../Sources/ChatListController.swift | 58 +- .../Sources/ChatListControllerNode.swift | 32 +- .../ChatListFilterPresetController.swift | 70 +-- .../ChatListFilterPresetListController.swift | 46 +- .../ChatListFilterTabContainerNode.swift | 10 +- .../Sources/Node/ChatListItem.swift | 2 +- .../Sources/Node/ChatListNode.swift | 13 +- .../TabBarChatListFilterController.swift | 10 +- .../ContextUI/Sources/ContextController.swift | 1 + submodules/Display/Source/NavigationBar.swift | 10 +- submodules/SyncCore/Sources/Namespaces.swift | 1 + submodules/TelegramCore/Sources/Account.swift | 2 +- .../Sources/AccountIntermediateState.swift | 17 +- .../TelegramCore/Sources/AccountManager.swift | 1 + .../Sources/AccountStateManagementUtils.swift | 53 +- .../Sources/ChatListFiltering.swift | 557 ++++++++++++++---- .../ItemList.imageset/Contents.json | 12 + .../ItemList.imageset/ic_list.pdf | Bin 0 -> 4167 bytes .../ChatListFilters.imageset/ic_filters.pdf | Bin 4437 -> 4435 bytes 20 files changed, 658 insertions(+), 252 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ItemList.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ItemList.imageset/ic_list.pdf diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm index e8607e273c..f17f390aff 100644 --- a/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm @@ -498,7 +498,9 @@ ASSynthesizeLockingMethodsWithMutex(__instanceLock__); ASAssertLocked(__instanceLock__); UIView *view = nil; + bool initializedWithCustomView = false; if (_viewBlock) { + initializedWithCustomView = true; view = _viewBlock(); ASDisplayNodeAssertNotNil(view, @"View block returned nil"); ASDisplayNodeAssert(![view isKindOfClass:[_ASDisplayView class]], @"View block should return a synchronously displayed view"); @@ -518,6 +520,19 @@ ASSynthesizeLockingMethodsWithMutex(__instanceLock__); _flags.canCallSetNeedsDisplayOfLayer = NO; } + if (initializedWithCustomView) { + static dispatch_once_t onceToken; + static IMP defaultMethod = NULL; + dispatch_once(&onceToken, ^{ + defaultMethod = [[UIView class] instanceMethodForSelector:@selector(drawRect:)]; + }); + if ([[view class] instanceMethodForSelector:@selector(drawRect:)] != defaultMethod) { + } else { + _flags.canClearContentsOfLayer = NO; + _flags.canCallSetNeedsDisplayOfLayer = NO; + } + } + // UIActivityIndicator if ([_viewClass isSubclassOfClass:[UIActivityIndicatorView class]] || [_viewClass isSubclassOfClass:[UIVisualEffectView class]]) { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5f7772083f..2271813bfb 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -468,10 +468,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, guard let strongSelf = self else { return } - let _ = (strongSelf.context.account.postbox.transaction { transaction -> [ChatListFilter] in - let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default - return settings.filters - } + let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] filters in guard let strongSelf = self else { return @@ -514,10 +511,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, guard let strongSelf = self else { return } - let _ = (strongSelf.context.account.postbox.transaction { transaction -> [ChatListFilter] in - let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default - return settings.filters - } + let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] filters in guard let strongSelf = self else { return @@ -531,10 +525,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, guard let strongSelf = self else { return } - let _ = (strongSelf.context.account.postbox.transaction { transaction -> [ChatListFilter] in - let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default - return settings.filters - } + let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox) |> deliverOnMainQueue).start(next: { presetList in guard let strongSelf = self else { return @@ -563,10 +554,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, guard let strongSelf = self else { return } - let _ = (strongSelf.context.account.postbox.transaction { transaction -> [ChatListFilter] in - let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default - return settings.filters - } + let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox) |> deliverOnMainQueue).start(next: { presetList in guard let strongSelf = self else { return @@ -1150,17 +1138,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } strongSelf.processedFeaturedFilters = true if !featuredState.isSeen && !featuredState.filters.isEmpty { - let _ = (strongSelf.context.account.postbox.transaction { transaction -> Bool in - if let state = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState { - return !state.filters.isEmpty - } else { - return false - } - } - |> deliverOnMainQueue).start(next: { hasFilters in + let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox) + |> deliverOnMainQueue).start(next: { filters in guard let strongSelf = self else { return } + let hasFilters = !filters.isEmpty if let _ = strongSelf.validLayout, let parentController = strongSelf.parent as? TabBarController, let sourceFrame = parentController.frameForControllerTab(controller: strongSelf) { let absoluteFrame = sourceFrame //TODO:localize @@ -1295,29 +1278,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, @objc private func reorderingDonePressed() { if let reorderedFilterIds = self.tabContainerNode.reorderedFilterIds { - let _ = (updateChatListFilterSettingsInteractively(postbox: self.context.account.postbox, { state in - var state = state + let _ = (updateChatListFiltersInteractively(postbox: self.context.account.postbox, { stateFilters in var updatedFilters: [ChatListFilter] = [] for id in reorderedFilterIds { - if let index = state.filters.firstIndex(where: { $0.id == id }) { - updatedFilters.append(state.filters[index]) + if let index = stateFilters.firstIndex(where: { $0.id == id }) { + updatedFilters.append(stateFilters[index]) } } - updatedFilters.append(contentsOf: state.filters.compactMap { filter -> ChatListFilter? in + updatedFilters.append(contentsOf: stateFilters.compactMap { filter -> ChatListFilter? in if !updatedFilters.contains(where: { $0.id == filter.id }) { return filter } else { return nil } }) - state.filters = updatedFilters - return state + return updatedFilters }) |> 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 @@ -1451,12 +1431,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } } - let _ = updateChatListFilterSettingsInteractively(postbox: strongSelf.context.account.postbox, { settings in - var settings = settings - settings.filters = settings.filters.filter({ $0.id != id }) - return settings + let _ = updateChatListFiltersInteractively(postbox: strongSelf.context.account.postbox, { filters in + return filters.filter({ $0.id != id }) }).start() - let _ = replaceRemoteChatListFilters(account: strongSelf.context.account).start() } if strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id == id { @@ -2332,10 +2309,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) { let _ = (combineLatest(queue: .mainQueue(), - self.context.account.postbox.transaction { transaction -> [ChatListFilter] in - let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default - return settings.filters - }, + currentChatListFilters(postbox: self.context.account.postbox), chatListFilterItems(context: self.context) |> take(1) ) @@ -2350,7 +2324,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: presetList.isEmpty ? "Add Folder" : "Edit Folders", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + return generateTintedImage(image: UIImage(bundleImageName: presetList.isEmpty ? "Chat/Context Menu/Add" : "Chat/Context Menu/ItemList"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { guard let strongSelf = self else { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index daafddb2f3..16fc512da7 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -539,20 +539,10 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.applyItemNodeAsCurrent(id: .all, itemNode: itemNode) let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in - guard let strongSelf = self, let index = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }) else { + guard let strongSelf = self, strongSelf.availableFilters.count > 1 else { return [] } - var directions: InteractiveTransitionGestureRecognizerDirections = [.leftCenter, .rightCenter] - if strongSelf.availableFilters.count > 1 { - if index == 0 { - directions.remove(.rightCenter) - } - if index == strongSelf.availableFilters.count - 1 { - directions.remove(.leftCenter) - } - } else { - directions = [] - } + let directions: InteractiveTransitionGestureRecognizerDirections = [.leftCenter, .rightCenter] return directions }, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0)) panRecognizer.delegate = self @@ -601,11 +591,21 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { 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 { - transitionFraction = min(0.0, transitionFraction) + + func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { + let bandedOffset = offset - bandingStart + let range: CGFloat = 600.0 + let coefficient: CGFloat = 0.4 + return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range } - if selectedIndex >= self.availableFilters.count - 1 { - transitionFraction = max(0.0, transitionFraction) + + if selectedIndex <= 0 && translation.x > 0.0 { + let overscroll = translation.x + transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width + } + if selectedIndex >= self.availableFilters.count - 1 && translation.x < 0.0 { + let overscroll = -translation.x + transitionFraction = -rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width } self.transitionFraction = transitionFraction + self.transitionFractionOffset if let currentItemNode = self.currentItemNodeValue { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 53abcae843..2d20a73f7d 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -574,21 +574,19 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f } if applyAutomatically { - let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - for i in 0 ..< settings.filters.count { - if settings.filters[i].id == filter.id { - settings.filters[i].data.categories = categories - settings.filters[i].data.includePeers = includePeers - settings.filters[i].data.excludePeers = settings.filters[i].data.excludePeers.filter { !settings.filters[i].data.includePeers.contains($0) } + let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + var filters = filters + for i in 0 ..< filters.count { + if filters[i].id == filter.id { + filters[i].data.categories = categories + filters[i].data.includePeers = includePeers + filters[i].data.excludePeers = filters[i].data.excludePeers.filter { !filters[i].data.includePeers.contains($0) } } } - return settings + return filters }) - |> deliverOnMainQueue).start(next: { settings in + |> deliverOnMainQueue).start(next: { _ in controller?.dismiss() - - let _ = replaceRemoteChatListFilters(account: context.account).start() }) } else { var filter = filter @@ -653,23 +651,21 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex excludePeers.sort() if applyAutomatically { - let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - for i in 0 ..< settings.filters.count { - if settings.filters[i].id == filter.id { - settings.filters[i].data.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue) - settings.filters[i].data.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue) - settings.filters[i].data.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue) - settings.filters[i].data.excludePeers = excludePeers - settings.filters[i].data.includePeers = settings.filters[i].data.includePeers.filter { !settings.filters[i].data.excludePeers.contains($0) } + let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + var filters = filters + for i in 0 ..< filters.count { + if filters[i].id == filter.id { + filters[i].data.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue) + filters[i].data.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue) + filters[i].data.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue) + filters[i].data.excludePeers = excludePeers + filters[i].data.includePeers = filters[i].data.includePeers.filter { !filters[i].data.excludePeers.contains($0) } } } - return settings + return filters }) - |> deliverOnMainQueue).start(next: { settings in + |> deliverOnMainQueue).start(next: { _ in controller?.dismiss() - - let _ = replaceRemoteChatListFilters(account: context.account).start() }) } else { var filter = filter @@ -921,39 +917,37 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat var applyImpl: (() -> Void)? = { let state = stateValue.with { $0 } let preset = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: state.additionallyIncludePeers, excludePeers: state.additionallyExcludePeers)) - let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in + let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in var preset = preset if currentPreset == nil { - preset.id = max(2, settings.filters.map({ $0.id + 1 }).max() ?? 2) + preset.id = max(2, filters.map({ $0.id + 1 }).max() ?? 2) } - var settings = settings + var filters = filters if let _ = currentPreset { var found = false - for i in 0 ..< settings.filters.count { - if settings.filters[i].id == preset.id { - settings.filters[i] = preset + for i in 0 ..< filters.count { + if filters[i].id == preset.id { + filters[i] = preset found = true } } if !found { - settings.filters = settings.filters.filter { listFilter in + filters = filters.filter { listFilter in if listFilter.title == preset.title && listFilter.data == preset.data { return false } return true } - settings.filters.append(preset) + filters.append(preset) } } else { - settings.filters.append(preset) + filters.append(preset) } - return settings + return filters }) - |> deliverOnMainQueue).start(next: { settings in - updated(settings.filters) + |> deliverOnMainQueue).start(next: { filters in + updated(filters) dismissImpl?() - - let _ = replaceRemoteChatListFilters(account: context.account).start() }) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 4d2a6b13c7..971a515f02 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -248,13 +248,13 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let arguments = ChatListFilterPresetListControllerArguments(context: context, addSuggestedPresed: { title, data in - let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.filters.insert(ChatListFilter(id: max(2, settings.filters.map({ $0.id + 1 }).max() ?? 2), title: title, data: data), at: 0) - return settings + let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + var filters = filters + let id = generateNewChatListFilterId(filters: filters) + filters.insert(ChatListFilter(id: id, title: title, data: data), at: 0) + return filters }) - |> deliverOnMainQueue).start(next: { settings in - let _ = replaceRemoteChatListFilters(account: context.account).start() + |> deliverOnMainQueue).start(next: { _ in }) }, openPreset: { preset in pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: { _ in })) @@ -269,25 +269,20 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch return state } }, removePreset: { id in - let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - if let index = settings.filters.firstIndex(where: { $0.id == id }) { - settings.filters.remove(at: index) + let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + var filters = filters + if let index = filters.firstIndex(where: { $0.id == id }) { + filters.remove(at: index) } - return settings + return filters }) - |> deliverOnMainQueue).start(next: { settings in - let _ = replaceRemoteChatListFilters(account: context.account).start() + |> deliverOnMainQueue).start(next: { _ in }) }) let chatCountCache = Atomic<[ChatListFilterData: Int]>(value: [:]) - let filtersWithCountsSignal = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) - |> map { preferences -> [ChatListFilter] in - let filtersState = preferences.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState ?? ChatListFiltersState.default - return filtersState.filters - } + let filtersWithCountsSignal = updatedChatListFilters(postbox: context.account.postbox) |> distinctUntilChanged |> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in return .single(filters.map { filter -> (ChatListFilter, Int) in @@ -355,24 +350,20 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch |> take(1) |> deliverOnMainQueue).start(next: { [weak updatedFilterOrder] updatedFilterOrderValue in if let updatedFilterOrderValue = updatedFilterOrderValue { - let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { filtersState in - var filtersState = filtersState - + let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in var updatedFilters: [ChatListFilter] = [] for id in updatedFilterOrderValue { - if let index = filtersState.filters.firstIndex(where: { $0.id == id }) { - updatedFilters.append(filtersState.filters[index]) + if let index = filters.firstIndex(where: { $0.id == id }) { + updatedFilters.append(filters[index]) } } - for filter in filtersState.filters { + for filter in filters { if !updatedFilters.contains(where: { $0.id == filter.id }) { updatedFilters.append(filter) } } - filtersState.filters = updatedFilters - - return filtersState + return updatedFilters }) |> deliverOnMainQueue).start(next: { _ in filtersWithCounts.set(filtersWithCountsSignal) @@ -425,7 +416,6 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch controller.navigationPresentation = .modal } controller.didDisappear = { _ in - let _ = replaceRemoteChatListFilters(account: context.account).start() dismissed?() } pushControllerImpl = { [weak controller] c in diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 4c4738333b..87f91df2d3 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -697,10 +697,10 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { let minSpacing: CGFloat = 26.0 - let sideInset: CGFloat = 16.0 - var leftOffset: CGFloat = sideInset + let resolvedSideInset: CGFloat = 16.0 + sideInset + var leftOffset: CGFloat = resolvedSideInset - var longTitlesWidth: CGFloat = sideInset + var longTitlesWidth: CGFloat = resolvedSideInset for i in 0 ..< tabSizes.count { let (itemId, paneNodeSize, paneNodeShortSize, paneNode, wasAdded) = tabSizes[i] longTitlesWidth += paneNodeSize.width @@ -708,7 +708,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { longTitlesWidth += minSpacing } } - longTitlesWidth += sideInset + longTitlesWidth += resolvedSideInset let useShortTitles = longTitlesWidth > size.width for i in 0 ..< tabSizes.count { @@ -746,7 +746,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { leftOffset += paneNodeSize.width + minSpacing } leftOffset -= minSpacing - leftOffset += sideInset + leftOffset += resolvedSideInset self.scrollNode.view.contentSize = CGSize(width: leftOffset, height: size.height) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 33615bf3cb..b63019d418 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1665,7 +1665,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let leftInset: CGFloat = params.leftInset + avatarLeftInset - let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: self.bounds.size.height - 12.0 - 9.0)) + let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - editingOffset, height: self.bounds.size.height - 12.0 - 9.0)) let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + offset, dy: 0.0) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 97b078dcf5..6608a9ee51 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1243,15 +1243,10 @@ public final class ChatListNode: ListView { } private func resetFilter() { - if let chatListFilter = chatListFilter { - let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.chatListFilters])) - self.updatedFilterDisposable.set((context.account.postbox.combinedView(keys: [preferencesKey]) - |> map { view -> ChatListFilter? in - guard let preferencesView = view.views[preferencesKey] as? PreferencesView else { - return nil - } - let filersState = preferencesView.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState ?? ChatListFiltersState.default - for filter in filersState.filters { + if let chatListFilter = self.chatListFilter { + self.updatedFilterDisposable.set((updatedChatListFilters(postbox: self.context.account.postbox) + |> map { filters -> ChatListFilter? in + for filter in filters { if filter.id == chatListFilter.id { return filter } diff --git a/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift index 6b9737b946..2af6a41583 100644 --- a/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift +++ b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift @@ -11,15 +11,7 @@ import TelegramUIPreferences import TelegramCore func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilter, Int)]), NoError> { - let preferencesKey: PostboxViewKey = .preferences(keys: [PreferencesKeys.chatListFilters]) - return context.account.postbox.combinedView(keys: [preferencesKey]) - |> map { combinedView -> [ChatListFilter] in - if let filtersState = (combinedView.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState { - return filtersState.filters - } else { - return [] - } - } + return updatedChatListFilters(postbox: context.account.postbox) |> distinctUntilChanged |> mapToSignal { filters -> Signal<(Int, [(ChatListFilter, Int)]), NoError> in var unreadCountItems: [UnreadMessageCountsItem] = [] diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 955437af01..bf3b21f1e1 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1492,6 +1492,7 @@ public final class ContextController: ViewController, StandalonePresentableContr super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Hide + self.lockOrientation = true } required init(coder aDecoder: NSCoder) { diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 4389657b7b..8987abd847 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -873,8 +873,14 @@ open class NavigationBar: ASDisplayNode { let initialX: CGFloat = backButtonInset let finalX: CGFloat = floor((size.width - backButtonSize.width) / 2.0) - size.width - self.backButtonNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - backButtonSize.height) / 2.0)), size: backButtonSize) - self.backButtonNode.alpha = (1.0 - progress) * (1.0 - progress) + let backButtonFrame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - backButtonSize.height) / 2.0)), size: backButtonSize) + if self.backButtonNode.frame != backButtonFrame { + self.backButtonNode.frame = backButtonFrame + } + let backButtonAlpha = self.backButtonNode.alpha + if self.backButtonNode.alpha != backButtonAlpha { + self.backButtonNode.alpha = backButtonAlpha + } if let transitionTitleNode = self.transitionTitleNode { let transitionTitleSize = transitionTitleNode.measure(CGSize(width: size.width, height: nominalHeight)) diff --git a/submodules/SyncCore/Sources/Namespaces.swift b/submodules/SyncCore/Sources/Namespaces.swift index e332741483..514197c0e8 100644 --- a/submodules/SyncCore/Sources/Namespaces.swift +++ b/submodules/SyncCore/Sources/Namespaces.swift @@ -138,6 +138,7 @@ public struct OperationLogTags { public static let SynchronizeRecentlyUsedStickers = PeerOperationLogTag(value: 17) public static let SynchronizeAppLogEvents = PeerOperationLogTag(value: 18) public static let SynchronizeEmojiKeywords = PeerOperationLogTag(value: 19) + public static let SynchronizeChatListFilters = PeerOperationLogTag(value: 20) } public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable { diff --git a/submodules/TelegramCore/Sources/Account.swift b/submodules/TelegramCore/Sources/Account.swift index 4026a65b89..be82e24425 100644 --- a/submodules/TelegramCore/Sources/Account.swift +++ b/submodules/TelegramCore/Sources/Account.swift @@ -1044,7 +1044,7 @@ public class Account { self.managedOperationsDisposable.add(managedApplyPendingMessageReactionsActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) - self.managedOperationsDisposable.add(managedChatListFilters(postbox: self.postbox, network: self.network)) + self.managedOperationsDisposable.add(managedChatListFilters(postbox: self.postbox, network: self.network).start()) let importantBackgroundOperations: [Signal] = [ managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] }, diff --git a/submodules/TelegramCore/Sources/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/AccountIntermediateState.swift index 85ae665380..d3434c8689 100644 --- a/submodules/TelegramCore/Sources/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/AccountIntermediateState.swift @@ -97,6 +97,9 @@ enum AccountStateMutationOperation { case UpdatePeerChatInclusion(peerId: PeerId, groupId: PeerGroupId, changedGroup: Bool) case UpdatePeersNearby([PeerNearby]) case UpdateTheme(TelegramTheme) + case SyncChatListFilters + case UpdateChatListFilterOrder(order: [Int32]) + case UpdateChatListFilter(id: Int32, filter: Api.DialogFilter?) } struct AccountMutableState { @@ -408,9 +411,21 @@ struct AccountMutableState { self.addOperation(.UpdateCall(call)) } + mutating func addSyncChatListFilters() { + self.addOperation(.SyncChatListFilters) + } + + mutating func addUpdateChatListFilterOrder(order: [Int32]) { + self.addOperation(.UpdateChatListFilterOrder(order: order)) + } + + mutating func addUpdateChatListFilter(id: Int32, filter: Api.DialogFilter?) { + self.addOperation(.UpdateChatListFilter(id: id, filter: filter)) + } + mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter: break case let .AddMessages(messages, location): for message in messages { diff --git a/submodules/TelegramCore/Sources/AccountManager.swift b/submodules/TelegramCore/Sources/AccountManager.swift index 075e315698..2ae5a0733b 100644 --- a/submodules/TelegramCore/Sources/AccountManager.swift +++ b/submodules/TelegramCore/Sources/AccountManager.swift @@ -156,6 +156,7 @@ private var declaredEncodables: Void = { declareEncodable(PeersNearbyState.self, f: { PeersNearbyState(decoder: $0) }) declareEncodable(TelegramMediaDice.self, f: { TelegramMediaDice(decoder: $0) }) declareEncodable(ChatListFiltersFeaturedState.self, f: { ChatListFiltersFeaturedState(decoder: $0) }) + declareEncodable(SynchronizeChatListFiltersOperation.self, f: { SynchronizeChatListFiltersOperation(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift index 6dd6553c6f..0eb6714638 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift @@ -1320,6 +1320,12 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } case let .updateMessageID(id, randomId): updatedState.updatedOutgoingUniqueMessageIds[randomId] = id + case .updateDialogFilters: + updatedState.addSyncChatListFilters() + case let .updateDialogFilterOrder(order): + updatedState.addUpdateChatListFilterOrder(order: order) + case let .updateDialogFilter(_, id, filter): + updatedState.addUpdateChatListFilter(id: id, filter: filter) default: break } @@ -2042,7 +2048,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddScheduledMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -2126,6 +2132,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP var updatedThemes: [Int64: TelegramTheme] = [:] var delayNotificatonsUntil: Int32? var peerActivityTimestamps: [PeerId: Int32] = [:] + var syncChatListFilters = false var holesFromPreviousStateMessageIds: [MessageId] = [] @@ -2683,6 +2690,46 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP updatedPeersNearby = peersNearby case let .UpdateTheme(theme): updatedThemes[theme.id] = theme + case .SyncChatListFilters: + syncChatListFilters = true + case let .UpdateChatListFilterOrder(order): + if !syncChatListFilters { + let _ = updateChatListFiltersState(transaction: transaction, { state in + var state = state + if Set(state.filters.map { $0.id }) == Set(order) { + var updatedFilters: [ChatListFilter] = [] + for id in order { + if let filter = state.filters.first(where: { $0.id == id }) { + updatedFilters.append(filter) + } else { + assertionFailure() + } + } + state.filters = updatedFilters + state.remoteFilters = state.filters + } else { + syncChatListFilters = true + } + return state + }) + } + case let .UpdateChatListFilter(id, filter): + if !syncChatListFilters { + let _ = updateChatListFiltersState(transaction: transaction, { state in + var state = state + if let index = state.filters.firstIndex(where: { $0.id == id }) { + if let filter = filter { + state.filters[index] = ChatListFilter(apiFilter: filter) + } else { + state.filters.remove(at: index) + } + state.remoteFilters = state.filters + } else { + syncChatListFilters = true + } + return state + }) + } } } @@ -3003,5 +3050,9 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP } } + if syncChatListFilters { + requestChatListFiltersSync(transaction: transaction) + } + return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil) } diff --git a/submodules/TelegramCore/Sources/ChatListFiltering.swift b/submodules/TelegramCore/Sources/ChatListFiltering.swift index c673996a19..0a83654899 100644 --- a/submodules/TelegramCore/Sources/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/ChatListFiltering.swift @@ -226,8 +226,8 @@ public enum RequestUpdateChatListFilterError { case generic } -public func requestUpdateChatListFilter(account: Account, id: Int32, filter: ChatListFilter?) -> Signal { - return account.postbox.transaction { transaction -> Api.DialogFilter? in +public func requestUpdateChatListFilter(postbox: Postbox, network: Network, id: Int32, filter: ChatListFilter?) -> Signal { + return postbox.transaction { transaction -> Api.DialogFilter? in return filter?.apiFilter(transaction: transaction) } |> castError(RequestUpdateChatListFilterError.self) @@ -236,7 +236,7 @@ public func requestUpdateChatListFilter(account: Account, id: Int32, filter: Cha if inputFilter != nil { flags |= 1 << 0 } - return account.network.request(Api.functions.messages.updateDialogFilter(flags: flags, id: id, filter: inputFilter)) + return network.request(Api.functions.messages.updateDialogFilter(flags: flags, id: id, filter: inputFilter)) |> mapError { _ -> RequestUpdateChatListFilterError in return .generic } @@ -324,118 +324,143 @@ private func requestChatListFilters(postbox: Postbox, network: Network) -> Signa |> castError(RequestChatListFiltersError.self) |> mapToSignal { filtersAndMissingPeers -> Signal<[ChatListFilter], RequestChatListFiltersError> in let (filters, missingPeers) = filtersAndMissingPeers - return .single(filters) - } - } -} - -func managedChatListFilters(postbox: Postbox, network: Network) -> Disposable { - let disposables = DisposableSet() - disposables.add(updateChatListFeaturedFilters(postbox: postbox, network: network).start()) - - disposables.add((requestChatListFilters(postbox: postbox, network: network) - |> `catch` { _ -> Signal<[ChatListFilter], NoError> in - return .complete() - } - |> mapToSignal { filters -> Signal in - return postbox.transaction { transaction in - transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in - var settings = entry as? ChatListFiltersState ?? ChatListFiltersState.default - settings.filters = filters - return settings - }) - } - |> ignoreValues - }).start()) - - return disposables -} - -public func replaceRemoteChatListFilters(account: Account) -> Signal { - return account.postbox.transaction { transaction -> [ChatListFilter] in - let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default - return settings.filters - } - |> mapToSignal { filters -> Signal in - return requestChatListFilters(postbox: account.postbox, network: account.network) - |> `catch` { _ -> Signal<[ChatListFilter], NoError> in - return .complete() - } - |> mapToSignal { remoteFilters -> Signal in - var deleteSignals: [Signal] = [] - for filter in remoteFilters { - if !filters.contains(where: { $0.id == filter.id }) { - deleteSignals.append(requestUpdateChatListFilter(account: account, id: filter.id, filter: nil) - |> `catch` { _ -> Signal in - return .complete() - } - |> ignoreValues) + + var missingUsers: [Api.InputUser] = [] + var missingChannels: [Api.InputChannel] = [] + var missingGroups: [Int32] = [] + for peer in missingPeers { + switch peer { + case let .inputPeerUser(userId, accessHash): + missingUsers.append(.inputUser(userId: userId, accessHash: accessHash)) + case .inputPeerSelf: + missingUsers.append(.inputUserSelf) + case let .inputPeerChannel(channelId, accessHash): + missingChannels.append(.inputChannel(channelId: channelId, accessHash: accessHash)) + case let .inputPeerChat(id): + missingGroups.append(id) + case .inputPeerEmpty: + break } } - let addFilters = account.postbox.transaction { transaction -> [(Int32, ChatListFilter)] in - let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default - return settings.filters.map { filter -> (Int32, ChatListFilter) in - return (filter.id, filter) + let resolveMissingUsers: Signal + if !missingUsers.isEmpty { + resolveMissingUsers = network.request(Api.functions.users.getUsers(id: missingUsers)) + |> `catch` { _ -> Signal<[Api.User], NoError> in + return .single([]) } - } - |> mapToSignal { filters -> Signal in - var signals: [Signal] = [] - for (id, filter) in filters { - if !remoteFilters.contains(filter) { - signals.append(requestUpdateChatListFilter(account: account, id: id, filter: filter) - |> `catch` { _ -> Signal in - return .complete() + |> mapToSignal { users -> Signal in + return postbox.transaction { transaction -> Void in + var peers: [Peer] = [] + for user in users { + peers.append(TelegramUser(user: user)) } - |> ignoreValues) + updatePeers(transaction: transaction, peers: peers, update: { _, updated in + return updated + }) } - } - return combineLatest(signals) - |> ignoreValues - } - - let reorderFilters: Signal - if remoteFilters.map({ $0.id }) != filters.map({ $0.id }) { - reorderFilters = account.network.request(Api.functions.messages.updateDialogFiltersOrder(order: filters.map { $0.id })) - |> ignoreValues - |> `catch` { _ -> Signal in - return .complete() + |> ignoreValues } } else { - reorderFilters = .complete() + resolveMissingUsers = .complete() } - return combineLatest( - deleteSignals - ) - |> ignoreValues - |> then( - addFilters + let resolveMissingChannels: Signal + if !missingChannels.isEmpty { + resolveMissingChannels = network.request(Api.functions.channels.getChannels(id: missingChannels)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Void in + if let result = result { + var peers: [Peer] = [] + switch result { + case .chats(let chats), .chatsSlice(_, let chats): + for chat in chats { + if let peer = parseTelegramGroupOrChannel(chat: chat) { + peers.append(peer) + } + } + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated in + return updated + }) + } + } + |> ignoreValues + } + } else { + resolveMissingChannels = .complete() + } + + let resolveMissingGroups: Signal + if !missingGroups.isEmpty { + resolveMissingGroups = network.request(Api.functions.messages.getChats(id: missingGroups)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Void in + if let result = result { + var peers: [Peer] = [] + switch result { + case .chats(let chats), .chatsSlice(_, let chats): + for chat in chats { + if let peer = parseTelegramGroupOrChannel(chat: chat) { + peers.append(peer) + } + } + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated in + return updated + }) + } + } + |> ignoreValues + } + } else { + resolveMissingGroups = .complete() + } + + return ( + resolveMissingUsers ) |> then( - reorderFilters + resolveMissingChannels + ) + |> then( + resolveMissingGroups + ) + |> castError(RequestChatListFiltersError.self) + |> mapToSignal { _ -> Signal<[ChatListFilter], RequestChatListFiltersError> in + } + |> then( + .single(filters) ) } } } -public struct ChatListFiltersState: PreferencesEntry, Equatable { - public var filters: [ChatListFilter] - public var remoteFilters: [ChatListFilter]? +struct ChatListFiltersState: PreferencesEntry, Equatable { + var filters: [ChatListFilter] + var remoteFilters: [ChatListFilter]? - public static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil) + static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil) - public init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?) { + fileprivate init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?) { self.filters = filters self.remoteFilters = remoteFilters } - public init(decoder: PostboxDecoder) { + init(decoder: PostboxDecoder) { self.filters = decoder.decodeObjectArrayWithDecoderForKey("filters") self.remoteFilters = decoder.decodeOptionalObjectArrayWithDecoderForKey("remoteFilters") } - public func encode(_ encoder: PostboxEncoder) { + func encode(_ encoder: PostboxEncoder) { encoder.encodeObjectArray(self.filters, forKey: "filters") if let remoteFilters = self.remoteFilters { encoder.encodeObjectArray(remoteFilters, forKey: "remoteFilters") @@ -444,7 +469,7 @@ public struct ChatListFiltersState: PreferencesEntry, Equatable { } } - public func isEqual(to: PreferencesEntry) -> Bool { + func isEqual(to: PreferencesEntry) -> Bool { if let to = to as? ChatListFiltersState, self == to { return true } else { @@ -453,19 +478,63 @@ public struct ChatListFiltersState: PreferencesEntry, Equatable { } } -public func updateChatListFilterSettingsInteractively(postbox: Postbox, _ f: @escaping (ChatListFiltersState) -> ChatListFiltersState) -> Signal { - return postbox.transaction { transaction -> ChatListFiltersState in - var result: ChatListFiltersState? - transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in - let settings = entry as? ChatListFiltersState ?? ChatListFiltersState.default - let updated = f(settings) - result = updated - return updated - }) - return result ?? .default +public func generateNewChatListFilterId(filters: [ChatListFilter]) -> Int32 { + while true { + let id = Int32(2 + arc4random_uniform(255 - 2)) + if !filters.contains(where: { $0.id == id }) { + return id + } } } +public func updateChatListFiltersInteractively(postbox: Postbox, _ f: @escaping ([ChatListFilter]) -> [ChatListFilter]) -> Signal<[ChatListFilter], NoError> { + return postbox.transaction { transaction -> [ChatListFilter] in + var updated: [ChatListFilter] = [] + var hasUpdates = false + transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in + var state = entry as? ChatListFiltersState ?? ChatListFiltersState.default + let updatedFilters = f(state.filters) + if updatedFilters != state.filters { + state.filters = updatedFilters + hasUpdates = true + } + updated = updatedFilters + return state + }) + if hasUpdates { + requestChatListFiltersSync(transaction: transaction) + } + return updated + } +} + +public func updatedChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], NoError> { + return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) + |> map { preferences -> [ChatListFilter] in + let filtersState = preferences.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState ?? ChatListFiltersState.default + return filtersState.filters + } + |> distinctUntilChanged +} + +public func currentChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], NoError> { + return postbox.transaction { transaction -> [ChatListFilter] in + let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default + return settings.filters + } +} + +func updateChatListFiltersState(transaction: Transaction, _ f: (ChatListFiltersState) -> ChatListFiltersState) -> ChatListFiltersState { + var result: ChatListFiltersState? + transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in + let settings = entry as? ChatListFiltersState ?? ChatListFiltersState.default + let updated = f(settings) + result = updated + return updated + }) + return result ?? .default +} + public struct ChatListFeaturedFilter: PostboxCoding, Equatable { public var title: String public var description: String @@ -579,3 +648,293 @@ public func updateChatListFeaturedFilters(postbox: Postbox, network: Network) -> |> ignoreValues } } + +private enum SynchronizeChatListFiltersOperationContentType: Int32 { + case add + case remove + case sync +} + +private enum SynchronizeChatListFiltersOperationContent: PostboxCoding { + case sync + + public init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("r", orElse: 0) { + case SynchronizeChatListFiltersOperationContentType.sync.rawValue: + self = .sync + default: + assertionFailure() + self = .sync + } + } + + public func encode(_ encoder: PostboxEncoder) { + switch self { + case .sync: + encoder.encodeInt32(SynchronizeChatListFiltersOperationContentType.sync.rawValue, forKey: "r") + } + } +} + +final class SynchronizeChatListFiltersOperation: PostboxCoding { + fileprivate let content: SynchronizeChatListFiltersOperationContent + + fileprivate init(content: SynchronizeChatListFiltersOperationContent) { + self.content = content + } + + init(decoder: PostboxDecoder) { + self.content = decoder.decodeObjectForKey("c", decoder: { SynchronizeChatListFiltersOperationContent(decoder: $0) }) as! SynchronizeChatListFiltersOperationContent + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.content, forKey: "c") + } +} + + +private final class ManagedSynchronizeChatListFiltersOperationsHelper { + var operationDisposables: [Int32: Disposable] = [:] + + func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) { + var disposeOperations: [Disposable] = [] + var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = [] + + var hasRunningOperationForPeerId = Set() + var validMergedIndices = Set() + for entry in entries { + if !hasRunningOperationForPeerId.contains(entry.peerId) { + hasRunningOperationForPeerId.insert(entry.peerId) + validMergedIndices.insert(entry.mergedIndex) + + if self.operationDisposables[entry.mergedIndex] == nil { + let disposable = MetaDisposable() + beginOperations.append((entry, disposable)) + self.operationDisposables[entry.mergedIndex] = disposable + } + } + } + + var removeMergedIndices: [Int32] = [] + for (mergedIndex, disposable) in self.operationDisposables { + if !validMergedIndices.contains(mergedIndex) { + removeMergedIndices.append(mergedIndex) + disposeOperations.append(disposable) + } + } + + for mergedIndex in removeMergedIndices { + self.operationDisposables.removeValue(forKey: mergedIndex) + } + + return (disposeOperations, beginOperations) + } + + func reset() -> [Disposable] { + let disposables = Array(self.operationDisposables.values) + self.operationDisposables.removeAll() + return disposables + } +} + +private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal) -> Signal { + return postbox.transaction { transaction -> Signal in + var result: PeerMergedOperationLogEntry? + transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in + if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeChatListFiltersOperation { + result = entry.mergedEntry! + return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) + } else { + return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) + } + }) + + return f(transaction, result) + } + |> switchToLatest +} + +func requestChatListFiltersSync(transaction: Transaction) { + let tag: PeerOperationLogTag = OperationLogTags.SynchronizeChatListFilters + let peerId = PeerId(namespace: 0, id: 0) + + var topOperation: (SynchronizeChatListFiltersOperation, Int32)? + transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in + if let operation = entry.contents as? SynchronizeChatListFiltersOperation { + topOperation = (operation, entry.tagLocalIndex) + } + return false + }) + + if let (topOperation, topLocalIndex) = topOperation, case .sync = topOperation.content { + let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex) + } + + transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeChatListFiltersOperation(content: .sync)) +} + +func managedChatListFilters(postbox: Postbox, network: Network) -> Signal { + return Signal { _ in + let updateFeaturedDisposable = updateChatListFeaturedFilters(postbox: postbox, network: network).start() + let _ = postbox.transaction({ transaction in + requestChatListFiltersSync(transaction: transaction) + }).start() + + let tag: PeerOperationLogTag = OperationLogTags.SynchronizeChatListFilters + + let helper = Atomic(value: ManagedSynchronizeChatListFiltersOperationsHelper()) + + let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in + let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in + return helper.update(view.entries) + } + + for disposable in disposeOperations { + disposable.dispose() + } + + for (entry, disposable) in beginOperations { + let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal in + if let entry = entry { + if let operation = entry.contents as? SynchronizeChatListFiltersOperation { + return synchronizeChatListFilters(transaction: transaction, postbox: postbox, network: network, operation: operation) + } else { + assertionFailure() + } + } + return .complete() + }) + |> then( + postbox.transaction { transaction -> Void in + let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex) + } + |> ignoreValues + ) + + disposable.set(signal.start()) + } + }) + + return ActionDisposable { + updateFeaturedDisposable.dispose() + + let disposables = helper.with { helper -> [Disposable] in + return helper.reset() + } + for disposable in disposables { + disposable.dispose() + } + disposable.dispose() + } + } +} + +private func synchronizeChatListFilters(transaction: Transaction, postbox: Postbox, network: Network, operation: SynchronizeChatListFiltersOperation) -> Signal { + switch operation.content { + case .sync: + let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default + let localFilters = settings.filters + let locallyKnownRemoteFilters = settings.remoteFilters ?? [] + + return requestChatListFilters(postbox: postbox, network: network) + |> `catch` { _ -> Signal<[ChatListFilter], NoError> in + return .complete() + } + |> mapToSignal { remoteFilters -> Signal in + if localFilters == locallyKnownRemoteFilters { + return postbox.transaction { transaction -> Void in + let _ = updateChatListFiltersState(transaction: transaction, { state in + var state = state + state.filters = remoteFilters + state.remoteFilters = state.filters + return state + }) + } + |> ignoreValues + } + + let locallyKnownRemoteFilterIds = locallyKnownRemoteFilters.map { $0.id } + + let remoteFilterIds = remoteFilters.map { $0.id } + let remotelyAddedFilters = Set(remoteFilterIds).subtracting(Set(locallyKnownRemoteFilterIds)) + let remotelyRemovedFilters = Set(Set(locallyKnownRemoteFilterIds)).subtracting(remoteFilterIds) + + var mergedFilters = localFilters + + for id in remotelyRemovedFilters { + mergedFilters.removeAll(where: { $0.id == id }) + } + + for id in remotelyAddedFilters { + if let filter = remoteFilters.first(where: { $0.id == id }) { + if let index = mergedFilters.firstIndex(where: { $0.id == id }) { + mergedFilters[index] = filter + } else { + mergedFilters.append(filter) + } + } + } + + let mergedFilterIds = mergedFilters.map { $0.id } + + var deleteSignals: Signal = .complete() + for filter in remoteFilters { + if !mergedFilterIds.contains(where: { $0 == filter.id }) { + deleteSignals = deleteSignals + |> then( + requestUpdateChatListFilter(postbox: postbox, network: network, id: filter.id, filter: nil) + |> `catch` { _ -> Signal in + return .complete() + } + |> ignoreValues + ) + } + } + + var addSignals: Signal = .complete() + for filter in mergedFilters { + if !remoteFilters.contains(where: { $0.id == filter.id }) { + addSignals = addSignals + |> then( + requestUpdateChatListFilter(postbox: postbox, network: network, id: filter.id, filter: filter) + |> `catch` { _ -> Signal in + return .complete() + } + |> ignoreValues + ) + } + } + + let localFilterIds = localFilters.map { $0.id } + let reorderFilters: Signal + if mergedFilterIds != remoteFilterIds { + reorderFilters = network.request(Api.functions.messages.updateDialogFiltersOrder(order: mergedFilters.map { $0.id })) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + } else { + reorderFilters = .complete() + } + + return deleteSignals + |> then( + addSignals + ) + |> then( + reorderFilters + ) + |> then( + postbox.transaction { transaction -> Void in + let _ = updateChatListFiltersState(transaction: transaction, { state in + var state = state + state.filters = mergedFilters + state.remoteFilters = state.filters + return state + }) + } + |> ignoreValues + ) + } + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ItemList.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ItemList.imageset/Contents.json new file mode 100644 index 0000000000..f9fa203bb7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ItemList.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_list.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ItemList.imageset/ic_list.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ItemList.imageset/ic_list.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2f376dc2736a2e9366cb1b5795124b5ec996794f GIT binary patch literal 4167 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/ChatListFilters.imageset/ic_filters.pdf b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/ChatListFilters.imageset/ic_filters.pdf index 822dd556bb170c2b139296175f0d88114af530cf..3f0497030e62c05ecba6a46ae5795662c34a0eba 100644 GIT binary patch delta 1037 zcmcbrbXjRaZ2gh4N9Sh=Z+OI@Ea1T_wCA_?6yv{=2iv~8-F^A$h~D(mf4l82>VH}$ zC+8%jYkcMKCUqhW>Rst(mv=W~;j2*=e_)-)J!CyWD&v>XFIh3gh`= z`rWUuZv1T%cf4=H?YS(R@kR5OY%q1aJt;TpqxK`_BYVFlJg!U#u2Ivsj)@rvuKtU34e9zhg?)Ox`u3fdnY1>@AOAYNCOXSVp@+rmc^mkzY*nN^sP5tv^gIJ(Lw*Ke)s9_{Jos+|AK!h~cV7#x)vYQt|9Y;L^Jm4v3dtur zkF!ojzU{uux9GIhUg`TLr-k3GGC%%w(x>`z}5&{~~PSxFu)r-+xZ?xij4EzdU^0 zyyVN;FE3^t3j6V^c+H_Bfi`<>U)Wk180hYJu>Jbezb?UzkGCtIy>6O#=*aO9!`Ynm zhFw0Q`tys7D^7?l6K+i1J71@9=Bm=2*>^WefB$F0dOW#!+l;%Wd_Byai&*`AN{${& zSS2{Ew!7={#(SFAMSssrU%bcMVAs>*k99UYxZo+uzmmst&!oc#Za->R{+yHJy-?_Z z&AXmvNh@#NkY0L^E$xCEXXDzdExR1oEVobMi(@y-ll_`Gtq^N79yeH&c7J7S3)?t=mG}`RR z9Ll3@XkcJ!WM*g(WniEVM4AfvzWFIGi6yBD8ZK5w21W)3#xNz5Px4!7nkX26fI^-E z7nosSU}0j8E@o_QW;|I(KrPM40$s|$(g=%kZ0amc(e)ae8(^4cY++(Dd53^qyt$Ea zl7WGlp-EzLa+;B)k%@_kX{w2_fpM~lrKzcjksX%}K_v>Y3U+o}#U+VFB^5=fXTSTmaZ8t(E`) delta 1064 zcmcbtbX93WZ2gbP7T;zGZ+OI@Ea1T_wCA_?6yv{=2ieZ9n7ku-cZ}Y5{a-HoGt6ci zI(sfoZS()-sCCw2&1AK=ORgDhSY4lQSyB}{Pm4Kj@zRekq;;6et0q)=S8%_~v1>nm z`dL-!9lN`y-*W7{>p9_r>UU1AaN!NMN882sG2LUVG2<(gY|o9TKXNU4#}Sqp3^Tr{ zxO_=rT*cgMTJ|G4afg7p+YgCy!xQW7s%+;r+MckHNAvh(i9D;t=e2${oHL)>Gh4ji z-ZyDO^Jj^(*lv~WayWb^c1rsGMB|1HocuHR9*9rk_3(S5p0La8`s&yJWFI*?%*)Ia zektj4tL@FIcPsgXI+`Qxq z?{a|-@m&wDx%-D%INY3c&M}|As-LgKhgz%D7ea1)yg%{T>sbb(lag1t+^L_+ z;=1gm&RnnJj1$w(9&7Ns{##LM#>!jMcU|kc9r5!e>(R$W+azps`DU{SxjsDR5}w1e z%rfzGbcoEcy5%wccdCE6Wv)*y@$PTe&rwWl){+adEIQ-G$C%H}?B~xfU~jsrA-&Z5 z>b>fyoAa)oy=K^aubU%gt=bi)P4k|JL@KSVuU^kTaSLNqp`73Xw&(M@M5=El@g0pg zWVT1W)2vY{x!KjQJHcb)#~G#UrJ?KUeL9xpCUx=5*>bNvv|xt8?p+5{?ke`CdS|eS zU7UF!-*@+-_60(}6Bpa5xSoE^nd%tzBBZscqh>b*r)1vFYDmu&Ru#3t#%jf^^lZT@ z2eItRIkR2AdwZ|){BLYA!MaXDV8fHuQz!ho#rS^7qNwY|yeH&M+Ps}7Ycb0(nrwDw z4&~7{Ff=qUvNSS{GB8jFB25K--~1Gp#FA764HqjT10w?iW0;c3$N8-^%@hnkKp{_o z3(PPuurRSi7c(|DGo7p@pjK~efG%ZVX@o^NHg%Tf=z5LK4Kd6!wlG0*Qb|!_W=?7m zmyL~resE^h7Mk$77Mn;Af=4lqGMv2KOhNc!~X?9#T1eGYn eD%ja^6_+Fyl~fd^rg51Vm>F=Xs=E5SaRC6>*Qz7{