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 0000000000..2f376dc273 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ItemList.imageset/ic_list.pdf differ 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 822dd556bb..3f0497030e 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/ChatListFilters.imageset/ic_filters.pdf and b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/ChatListFilters.imageset/ic_filters.pdf differ