diff --git a/Makefile b/Makefile index a9c1e10f33..a78940e107 100644 --- a/Makefile +++ b/Makefile @@ -405,13 +405,24 @@ bazel_app_arm64: --output_groups=+dsyms \ --verbose_failures -bazel_project: kill_xcode +bazel_prepare_development_build: + APP_VERSION="${APP_VERSION}" \ + BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \ + build-system/prepare-build.sh development + +bazel_project: kill_xcode bazel_prepare_development_build APP_VERSION="${APP_VERSION}" \ BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \ build-system/generate-xcode-project.sh -bazel_soft_project: +bazel_soft_project: bazel_prepare_development_build APP_VERSION="${APP_VERSION}" \ BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \ build-system/generate-xcode-project.sh +bazel_opt_project: bazel_prepare_development_build + APP_VERSION="${APP_VERSION}" \ + BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \ + GENERATE_OPT_PROJECT=1 \ + build-system/generate-xcode-project.sh + diff --git a/Telegram/BUILD b/Telegram/BUILD index b757fa8d57..d438aecce4 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -27,6 +27,7 @@ load( "telegram_version", "telegram_bundle_id", "telegram_aps_environment", + "telegram_team_id", ) config_setting( @@ -231,6 +232,11 @@ official_apple_pay_merchants = [ "merchant.telegram.tranzzo.test", ] +official_bundle_ids = [ + "ph.telegra.Telegraph", + "org.telegram.Telegram-iOS", +] + apple_pay_merchants = official_apple_pay_merchants if telegram_bundle_id == "ph.telegra.Telegraph" else "" apple_pay_merchants_fragment = "" if apple_pay_merchants == "" else """ @@ -242,6 +248,12 @@ apple_pay_merchants_fragment = "" if apple_pay_merchants == "" else """ """ +official_unrestricted_voip_fragment = """ +com.apple.developer.pushkit.unrestricted-voip + +""" +unrestricted_voip_fragment = official_unrestricted_voip_fragment if telegram_bundle_id in official_bundle_ids else "" + telegram_entitlements_template = """ com.apple.developer.icloud-services @@ -265,15 +277,16 @@ telegram_entitlements_template = """ group.{telegram_bundle_id} - com.apple.developer.pushkit.unrestricted-voip - -""" + apple_pay_merchants_fragment + application-identifier + {telegram_team_id}.{telegram_bundle_id} +""" + apple_pay_merchants_fragment + unrestricted_voip_fragment plist_fragment( name = "TelegramEntitlements", extension = "entitlements", template = telegram_entitlements_template.format( telegram_bundle_id = telegram_bundle_id, + telegram_team_id = telegram_team_id, telegram_aps_environment = telegram_aps_environment, ) ) diff --git a/build-system/generate-xcode-project.sh b/build-system/generate-xcode-project.sh index 73826043c9..4cc7c0d8b1 100755 --- a/build-system/generate-xcode-project.sh +++ b/build-system/generate-xcode-project.sh @@ -20,6 +20,7 @@ EXPECTED_VARIABLES=(\ BUILD_NUMBER \ APP_VERSION \ BUNDLE_ID \ + DEVELOPMENT_TEAM \ API_ID \ API_HASH \ APP_CENTER_ID \ diff --git a/build-system/prepare-build-variables.sh b/build-system/prepare-build-variables.sh index fb098631a9..d88042f518 100755 --- a/build-system/prepare-build-variables.sh +++ b/build-system/prepare-build-variables.sh @@ -27,6 +27,7 @@ prepare_build_variables () { BUILD_NUMBER \ APP_VERSION \ BUNDLE_ID \ + DEVELOPMENT_TEAM \ API_ID \ API_HASH \ APP_CENTER_ID \ @@ -57,6 +58,7 @@ prepare_build_variables () { echo "telegram_version = \"$APP_VERSION\"" >> "$VARIABLES_PATH" echo "telegram_bundle_id = \"$BUNDLE_ID\"" >> "$VARIABLES_PATH" echo "telegram_api_id = \"$API_ID\"" >> "$VARIABLES_PATH" + echo "telegram_team_id = \"$DEVELOPMENT_TEAM\"" >> "$VARIABLES_PATH" echo "telegram_api_hash = \"$API_HASH\"" >> "$VARIABLES_PATH" echo "telegram_app_center_id = \"$APP_CENTER_ID\"" >> "$VARIABLES_PATH" echo "telegram_is_internal_build = \"$IS_INTERNAL_BUILD\"" >> "$VARIABLES_PATH" diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index b937c1e766..afb6f7776c 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -134,7 +134,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, private var presentationDataDisposable: Disposable? private let stateDisposable = MetaDisposable() - private var filterDisposable = MetaDisposable() + private let filterDisposable = MetaDisposable() + private let featuredFiltersDisposable = MetaDisposable() + private var processedFeaturedFilters = false private let isReorderingTabsValue = ValuePromise(false) @@ -298,8 +300,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, isRoot = true if isReorderingTabs { - let rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.reorderingDonePressed)) - strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem + strongSelf.navigationItem.rightBarButtonItem = nil } else { let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed)) rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose @@ -307,7 +308,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } if isReorderingTabs { - strongSelf.navigationItem.leftBarButtonItem = nil + let leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.reorderingDonePressed)) + strongSelf.navigationItem.leftBarButtonItem = leftBarButtonItem } else { let editItem: UIBarButtonItem if state.editing { @@ -616,6 +618,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self.presentationDataDisposable?.dispose() self.stateDisposable.dispose() self.filterDisposable.dispose() + self.featuredFiltersDisposable.dispose() } private func updateThemeAndStrings() { @@ -999,6 +1002,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self.didAppear = true + self.chatListDisplayNode.containerNode.updateEnableAdjacentFilterLoading(true) + guard case .root = self.groupId else { return } @@ -1088,14 +1093,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, _ = markSuggestedLocalizationAsSeenInteractively(postbox: strongSelf.context.account.postbox, languageCode: suggestedLocalization.languageCode).start() } })) - - Queue.mainQueue().after(1.0, { - if let _ = self.validLayout, let parentController = self.parent as? TabBarController, let sourceFrame = parentController.frameForControllerTab(controller: self) { - let absoluteFrame = sourceFrame - //TODO:localize - //parentController.present(TooltipScreen(text: "Hold the Chats icon for quick access to the list of chat filters.", location: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0)), in: .current) - } - }) } self.chatListDisplayNode.containerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in @@ -1117,17 +1114,61 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return true }) } + + if !self.processedFeaturedFilters { + self.featuredFiltersDisposable.set(( + self.context.account.postbox.transaction { transaction -> ChatListFiltersFeaturedState? in + return transaction.getPreferencesEntry(key: PreferencesKeys.chatListFiltersFeaturedState) as? ChatListFiltersFeaturedState + } + |> delay(1.0, queue: .mainQueue()) + |> deliverOnMainQueue + ).start(next: { [weak self] featuredState in + guard let strongSelf = self, let featuredState = featuredState else { + return + } + 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 + guard let strongSelf = self else { + return + } + if let _ = strongSelf.validLayout, let parentController = strongSelf.parent as? TabBarController, let sourceFrame = parentController.frameForControllerTab(controller: strongSelf) { + let absoluteFrame = sourceFrame + //TODO:localize + let text: String + if hasFilters { + text = "Hold on 'Chats' to edit folders and switch between views." + } else { + text = "Hold to organize your chats with folders." + } + parentController.present(TooltipScreen(text: text, location: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0)), in: .current) + } + }) + } + })) + } } override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + self.chatListDisplayNode.containerNode.updateEnableAdjacentFilterLoading(false) + self.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() } return true }) + + self.featuredFiltersDisposable.set(nil) } override public func viewDidDisappear(_ animated: Bool) { @@ -1368,12 +1409,33 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, guard let strongSelf = self else { return } - let _ = updateChatListFilterSettingsInteractively(postbox: strongSelf.context.account.postbox, { settings in - var settings = settings - settings.filters = settings.filters.filter({ $0.id != id }) - return settings - }).start() - let _ = replaceRemoteChatListFilters(account: strongSelf.context.account).start() + + let commit: () -> Void = { + guard let strongSelf = self else { + return + } + + if strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id == id { + if strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing { + strongSelf.donePressed() + } + } + + let _ = updateChatListFilterSettingsInteractively(postbox: strongSelf.context.account.postbox, { settings in + var settings = settings + settings.filters = settings.filters.filter({ $0.id != id }) + return settings + }).start() + let _ = replaceRemoteChatListFilters(account: strongSelf.context.account).start() + } + + if strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id == id { + strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: .all, completion: { + commit() + }) + } else { + commit() + } }) ]), ActionSheetItemGroup(items: [ @@ -2232,7 +2294,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } private func openFilterSettings() { - self.push(chatListFilterPresetListController(context: self.context, updated: { _ in + self.chatListDisplayNode.containerNode.updateEnableAdjacentFilterLoading(false) + self.push(chatListFilterPresetListController(context: self.context, mode: .modal, dismissed: { [weak self] in + self?.chatListDisplayNode.containerNode.updateEnableAdjacentFilterLoading(true) })) } @@ -2250,6 +2314,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return } + let _ = markChatListFeaturedFiltersAsSeen(postbox: strongSelf.context.account.postbox).start() + let (_, filterItems) = filterItemsAndTotalCount var items: [ContextMenuItem] = [] @@ -2260,7 +2326,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, guard let strongSelf = self else { return } - strongSelf.push(chatListFilterPresetListController(context: strongSelf.context, updated: { _ in })) + strongSelf.openFilterSettings() }) }))) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 446d9d5423..843763be3c 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -89,6 +89,8 @@ private final class ChatListContainerItemNode: ASDisplayNode { emptyNode.frame = emptyNodeFrame emptyNode.updateLayout(size: emptyNodeFrame.size, transition: .immediate) } + emptyNode.alpha = 0.0 + transition.updateAlpha(node: emptyNode, alpha: 1.0) } if !isLoading { strongSelf.becameEmpty(filter) @@ -149,6 +151,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private var disableItemNodeOperationsWhileAnimating: Bool = false private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, isReorderingFilters: Bool, isEditing: Bool)? + private var enableAdjacentFilterLoading: Bool = false + private var panRecognizer: InteractiveTransitionGestureRecognizer? private let _ready = Promise() @@ -458,7 +462,17 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } - func switchToFilter(id: ChatListFilterTabEntryId) { + func updateEnableAdjacentFilterLoading(_ value: Bool) { + if value != self.enableAdjacentFilterLoading { + self.enableAdjacentFilterLoading = value + + if self.enableAdjacentFilterLoading, let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout { + self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate) + } + } + } + + func switchToFilter(id: ChatListFilterTabEntryId, completion: (() -> Void)? = nil) { guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout else { return } @@ -472,6 +486,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: transition) self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false) + completion?() } else if self.pendingItemNode == nil { let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[index].filter, previewing: self.previewing, presentationData: self.presentationData, becameEmpty: { [weak self] filter in self?.filterBecameEmpty(filter) @@ -549,6 +564,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, transition, false) } + + completion?() })) } } @@ -574,7 +591,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { let id = self.availableFilters[i].id validNodeIds.append(id) - if self.itemNodes[id] == nil && !self.disableItemNodeOperationsWhileAnimating { + if self.itemNodes[id] == nil && self.enableAdjacentFilterLoading && !self.disableItemNodeOperationsWhileAnimating { let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[i].filter, previewing: self.previewing, presentationData: self.presentationData, becameEmpty: { [weak self] filter in self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 092a2aaec1..8a2d3a7e3f 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -58,12 +58,20 @@ private enum ChatListFilterPresetListEntryStableId: Hashable { case listFooter } +private struct PresetIndex: Equatable { + let value: Int + + static func ==(lhs: PresetIndex, rhs: PresetIndex) -> Bool { + return true + } +} + private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case screenHeader(String) case suggestedListHeader(String) - case suggestedPreset(index: Int, title: String, label: String, preset: ChatListFilterData) + case suggestedPreset(index: PresetIndex, title: String, label: String, preset: ChatListFilterData) case listHeader(String) - case preset(index: Int, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool) + case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool) case addItem(text: String, isEditing: Bool) case listFooter(String) @@ -85,7 +93,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case .listHeader: return 100 case let .preset(preset): - return 101 + preset.index + return 101 + preset.index.value case .addItem: return 1000 case .listFooter: @@ -93,7 +101,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case .suggestedListHeader: return 1002 case let .suggestedPreset(suggestedPreset): - return 1003 + suggestedPreset.index + return 1003 + suggestedPreset.index.value } } @@ -156,14 +164,30 @@ private struct ChatListFilterPresetListControllerState: Equatable { var revealedPreset: Int32? = nil } -private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], suggestedFilters: [(String, String, ChatListFilterData)], settings: ChatListFilterSettings) -> [ChatListFilterPresetListEntry] { +private func filtersWithAppliedOrder(filters: [(ChatListFilter, Int)], order: [Int32]?) -> [(ChatListFilter, Int)] { + let sortedFilters: [(ChatListFilter, Int)] + if let updatedFilterOrder = order { + var updatedFilters: [(ChatListFilter, Int)] = [] + for id in updatedFilterOrder { + if let index = filters.firstIndex(where: { $0.0.id == id }) { + updatedFilters.append(filters[index]) + } + } + sortedFilters = updatedFilters + } else { + sortedFilters = filters + } + return sortedFilters +} + +private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], settings: ChatListFilterSettings) -> [ChatListFilterPresetListEntry] { var entries: [ChatListFilterPresetListEntry] = [] entries.append(.screenHeader("Create folders for different groups of chats and\nquickly switch between them.")) - let filteredSuggestedFilters = suggestedFilters.filter { _, _, data in + let filteredSuggestedFilters = suggestedFilters.filter { suggestedFilter in for (filter, _) in filters { - if filter.data == data { + if filter.data == suggestedFilter.data { return false } } @@ -171,8 +195,9 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present } entries.append(.listHeader("FOLDERS")) - for (filter, chatCount) in filters { - entries.append(.preset(index: entries.count, title: filter.title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing)) + + for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { + entries.append(.preset(index: PresetIndex(value: entries.count), title: filter.title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing)) } if filters.count < 10 { entries.append(.addItem(text: "Create New Folder", isEditing: state.isEditing)) @@ -181,15 +206,20 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present if !filteredSuggestedFilters.isEmpty { entries.append(.suggestedListHeader("RECOMMENDED FOLDERS")) - for (title, label, data) in filteredSuggestedFilters { - entries.append(.suggestedPreset(index: entries.count, title: title, label: label, preset: data)) + for filter in filteredSuggestedFilters { + entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title, label: filter.description, preset: filter.data)) } } return entries } -public func chatListFilterPresetListController(context: AccountContext, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { +public enum ChatListFilterPresetListControllerMode { + case `default` + case modal +} + +public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, dismissed: (() -> Void)? = nil) -> ViewController { let initialState = ChatListFilterPresetListControllerState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -208,14 +238,12 @@ public func chatListFilterPresetListController(context: AccountContext, updated: return settings }) |> deliverOnMainQueue).start(next: { settings in - updated(settings.filters) - let _ = replaceRemoteChatListFilters(account: context.account).start() }) }, openPreset: { preset in - pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: updated)) + pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: { _ in })) }, addNew: { - pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: updated)) + pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in })) }, setItemWithRevealedOptions: { preset, fromPreset in updateState { state in var state = state @@ -233,22 +261,23 @@ public func chatListFilterPresetListController(context: AccountContext, updated: return settings }) |> deliverOnMainQueue).start(next: { settings in - updated(settings.filters) - let _ = replaceRemoteChatListFilters(account: context.account).start() }) }) let chatCountCache = Atomic<[ChatListFilterData: Int]>(value: [:]) - let filtersWithCounts = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) + 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 } |> distinctUntilChanged |> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in - return context.account.postbox.transaction { transaction -> [(ChatListFilter, Int)] in + return .single(filters.map { filter -> (ChatListFilter, Int) in + return (filter, 0) + }) + /*return context.account.postbox.transaction { transaction -> [(ChatListFilter, Int)] in return filters.map { filter -> (ChatListFilter, Int) in let count: Int if let cachedValue = chatCountCache.with({ dict -> Int? in @@ -265,36 +294,93 @@ public func chatListFilterPresetListController(context: AccountContext, updated: } return (filter, count) } - } + }*/ } - let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings]) + let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState]) + |> map { preferences -> [ChatListFeaturedFilter] in + guard let state = preferences.values[PreferencesKeys.chatListFiltersFeaturedState] as? ChatListFiltersFeaturedState else { + return [] + } + return state.filters + } + |> distinctUntilChanged - let suggestedFilters: [(String, String, ChatListFilterData)] = [ - ("Unread", "All unread chats", ChatListFilterData(categories: .all, excludeMuted: false, excludeRead: true, excludeArchived: false, includePeers: [], excludePeers: [])), - ("Personal", "Exclude groups and channels", ChatListFilterData(categories: ChatListFilterPeerCategories.all.subtracting([.groups, .channels]), excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: [], excludePeers: [])), - ] + let filtersWithCounts = Promise<[(ChatListFilter, Int)]>() + filtersWithCounts.set(filtersWithCountsSignal) + + let updatedFilterOrder = Promise<[Int32]?>(nil) + + let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings]) let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), - filtersWithCounts, - preferences + filtersWithCounts.get(), + preferences, + updatedFilterOrder.get(), + featuredFilters ) - |> map { presentationData, state, filtersWithCounts, preferences -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters -> (ItemListControllerState, (ItemListNodeState, Any)) in let filterSettings = preferences.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings] as? ChatListFilterSettings ?? ChatListFilterSettings.default - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Close), style: .regular, enabled: true, action: { - dismissImpl?() - }) + let leftNavigationButton: ItemListNavigationButton? + switch mode { + case .default: + leftNavigationButton = nil + case .modal: + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Close), style: .regular, enabled: true, action: { + dismissImpl?() + }) + } let rightNavigationButton: ItemListNavigationButton if state.isEditing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { - updateState { state in - var state = state - state.isEditing = false - return state - } - }) + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + let _ = (updatedFilterOrder.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak updatedFilterOrder] updatedFilterOrderValue in + if let updatedFilterOrderValue = updatedFilterOrderValue { + let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { filtersState in + var filtersState = filtersState + + var updatedFilters: [ChatListFilter] = [] + for id in updatedFilterOrderValue { + if let index = filtersState.filters.firstIndex(where: { $0.id == id }) { + updatedFilters.append(filtersState.filters[index]) + } + } + for filter in filtersState.filters { + if !updatedFilters.contains(where: { $0.id == filter.id }) { + updatedFilters.append(filter) + } + } + + filtersState.filters = updatedFilters + + return filtersState + }) + |> deliverOnMainQueue).start(next: { _ in + filtersWithCounts.set(filtersWithCountsSignal) + let _ = (filtersWithCounts.get() + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + updatedFilterOrder?.set(.single(nil)) + + updateState { state in + var state = state + state.isEditing = false + return state + } + }) + }) + } else { + updateState { state in + var state = state + state.isEditing = false + return state + } + } + }) + }) } else { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in @@ -306,7 +392,7 @@ public func chatListFilterPresetListController(context: AccountContext, updated: } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Folders"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCounts, suggestedFilters: suggestedFilters, settings: filterSettings), style: .blocks, animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, settings: filterSettings), style: .blocks, animateChanges: true) return (controllerState, (listState, arguments)) } @@ -314,9 +400,15 @@ public func chatListFilterPresetListController(context: AccountContext, updated: } let controller = ItemListController(context: context, state: signal) - controller.navigationPresentation = .modal - controller.willDisappear = { _ in + switch mode { + case .default: + controller.navigationPresentation = .default + case .modal: + controller.navigationPresentation = .modal + } + controller.didDisappear = { _ in let _ = replaceRemoteChatListFilters(account: context.account).start() + dismissed?() } pushControllerImpl = { [weak controller] c in controller?.push(c) @@ -347,36 +439,46 @@ public func chatListFilterPresetListController(context: AccountContext, updated: afterAll = true } - return updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { filtersState in - var filtersState = filtersState - if let index = filtersState.filters.firstIndex(where: { $0.id == fromFilter.preset.id }) { - filtersState.filters.remove(at: index) + return combineLatest( + updatedFilterOrder.get() |> take(1), + filtersWithCounts.get() |> take(1) + ) + |> mapToSignal { updatedFilterOrderValue, filtersWithCountsValue -> Signal in + var filters = filtersWithAppliedOrder(filters: filtersWithCountsValue, order: updatedFilterOrderValue).map { $0.0 } + let initialOrder = filters.map { $0.id } + + if let index = filters.firstIndex(where: { $0.id == fromFilter.preset.id }) { + filters.remove(at: index) } if let referenceFilter = referenceFilter { var inserted = false - for i in 0 ..< filtersState.filters.count { - if filtersState.filters[i].id == referenceFilter.id { + for i in 0 ..< filters.count { + if filters[i].id == referenceFilter.id { if fromIndex < toIndex { - filtersState.filters.insert(fromFilter.preset, at: i + 1) + filters.insert(fromFilter.preset, at: i + 1) } else { - filtersState.filters.insert(fromFilter.preset, at: i) + filters.insert(fromFilter.preset, at: i) } inserted = true break } } if !inserted { - filtersState.filters.append(fromFilter.preset) + filters.append(fromFilter.preset) } } else if beforeAll { - filtersState.filters.insert(fromFilter.preset, at: 0) + filters.insert(fromFilter.preset, at: 0) } else if afterAll { - filtersState.filters.append(fromFilter.preset) + filters.append(fromFilter.preset) + } + + let updatedOrder = filters.map { $0.id } + if initialOrder != updatedOrder { + updatedFilterOrder.set(.single(updatedOrder)) + return .single(true) + } else { + return .single(false) } - return filtersState - }) - |> map { _ -> Bool in - return false } }) diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 91a0a72887..e7d228ae38 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -205,10 +205,10 @@ private final class ItemNode: ASDisplayNode { } func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> (width: CGFloat, shortWidth: CGFloat) { - let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) - let shortTitleSize = self.shortTitleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let shortTitleSize = self.shortTitleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude)) self.shortTitleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - shortTitleSize.height) / 2.0)), size: shortTitleSize) if let deleteButtonNode = self.deleteButtonNode { @@ -609,10 +609,12 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { for filter in reorderedFilters { let itemNode: ItemNode + var itemNodeTransition = transition var wasAdded = false if let current = self.itemNodes[filter.id] { itemNode = current } else { + itemNodeTransition = .immediate wasAdded = true itemNode = ItemNode(pressed: { [weak self] in self?.tabSelected?(filter.id) @@ -646,7 +648,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { if !wasAdded && (itemNode.unreadCount != 0) != (unreadCount != 0) { badgeAnimations[filter.id] = (unreadCount != 0) ? .in : .out } - itemNode.updateText(title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, isNoFilter: isNoFilter, isSelected: selectedFilter == filter.id, isEditing: false, isAllChats: isNoFilter, isReordering: isEditing || isReordering, presentationData: presentationData, transition: transition) + itemNode.updateText(title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, isNoFilter: isNoFilter, isSelected: selectedFilter == filter.id, isEditing: false, isAllChats: isNoFilter, isReordering: isEditing || isReordering, presentationData: presentationData, transition: itemNodeTransition) } var removeKeys: [ChatListFilterTabEntryId] = [] for (id, _) in self.itemNodes { @@ -672,10 +674,12 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { continue } let wasAdded = itemNode.supernode == nil + var itemNodeTransition = transition if wasAdded { + itemNodeTransition = .immediate self.scrollNode.addSubnode(itemNode) } - let (paneNodeWidth, paneNodeShortWidth) = itemNode.updateLayout(height: size.height, transition: transition) + let (paneNodeWidth, paneNodeShortWidth) = itemNode.updateLayout(height: size.height, transition: itemNodeTransition) let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height) let paneNodeShortSize = CGSize(width: paneNodeShortWidth, height: size.height) tabSizes.append((filter.id, paneNodeSize, paneNodeShortSize, itemNode, wasAdded)) @@ -709,27 +713,32 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { for i in 0 ..< tabSizes.count { let (itemId, paneNodeLongSize, paneNodeShortSize, paneNode, wasAdded) = tabSizes[i] + var itemNodeTransition = transition + if wasAdded { + itemNodeTransition = .immediate + } + let useShortTitle = itemId == .all && useShortTitles let paneNodeSize = useShortTitle ? paneNodeShortSize : paneNodeLongSize let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) if itemId == self.reorderingItem, let (initial, offset) = self.reorderingItemPosition { - transition.updateSublayerTransformScale(node: paneNode, scale: 1.2) - transition.updateAlpha(node: paneNode, alpha: 0.9) - transition.updateFrameAdditive(node: paneNode, frame: CGRect(origin: CGPoint(x: initial + offset, y: paneFrame.minY), size: paneFrame.size)) + itemNodeTransition.updateSublayerTransformScale(node: paneNode, scale: 1.2) + itemNodeTransition.updateAlpha(node: paneNode, alpha: 0.9) + itemNodeTransition.updateFrameAdditive(node: paneNode, frame: CGRect(origin: CGPoint(x: initial + offset, y: paneFrame.minY), size: paneFrame.size)) } else { - transition.updateSublayerTransformScale(node: paneNode, scale: 1.0) - transition.updateAlpha(node: paneNode, alpha: 1.0) + itemNodeTransition.updateSublayerTransformScale(node: paneNode, scale: 1.0) + itemNodeTransition.updateAlpha(node: paneNode, alpha: 1.0) if wasAdded { paneNode.frame = paneFrame paneNode.alpha = 0.0 - transition.updateAlpha(node: paneNode, alpha: 1.0) + itemNodeTransition.updateAlpha(node: paneNode, alpha: 1.0) } else { - transition.updateFrameAdditive(node: paneNode, frame: paneFrame) + itemNodeTransition.updateFrameAdditive(node: paneNode, frame: paneFrame) } } - paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0, useShortTitle: useShortTitle, transition: transition) + paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0, useShortTitle: useShortTitle, transition: itemNodeTransition) paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing / 2.0, bottom: 0.0, right: -minSpacing / 2.0) selectionFrames.append(paneFrame) @@ -774,14 +783,11 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { } transition.updateAlpha(node: self.selectedLineNode, alpha: isReordering && selectedFilter == .all ? 0.5 : 1.0) - //if !transitionFraction.isZero { - if let previousSelectedFrame = self.previousSelectedFrame, abs(previousSelectedFrame.offsetBy(dx: -previousScrollBounds.minX, dy: 0.0).midX - previousScrollBounds.width / 2.0) < 1.0 { - let previousContentOffsetX = max(0.0, min(previousContentWidth - self.scrollNode.bounds.width, floor(previousSelectedFrame.midX - self.scrollNode.bounds.width / 2.0))) - if abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 { - focusOnSelectedFilter = true - } - } - //} + if let previousSelectedFrame = self.previousSelectedFrame { + let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0))) + focusOnSelectedFilter = abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 + } + if focusOnSelectedFilter && self.reorderingItem == nil { let updatedBounds: CGRect if transitionFraction.isZero && selectedFilter == reorderedFilters.first?.id { @@ -795,17 +801,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { self.scrollNode.bounds = updatedBounds } transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX) - /*else if false, !wasAdded, transitionFraction.isZero, let previousSelectedAbsFrame = self.previousSelectedAbsFrame { - let contentOffsetX: CGFloat - if previousScrollBounds.minX.isZero { - contentOffsetX = 0.0 - } else if previousScrollBounds.maxX == previousScrollBounds.width { - contentOffsetX = self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width - } else { - contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, selectedFrame.midX - previousSelectedAbsFrame.midX)) - } - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)) - }*/ + self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0) self.previousSelectedFrame = selectedFrame } else { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index d16b4400f9..7b07dd7e77 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1474,7 +1474,7 @@ public final class ChatListNode: ListView { } var scrollToItem: ListViewScrollToItem? switch self.visibleContentOffset() { - case let .known(value) where abs(value) < navigationBarSearchContentHeight: + case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0: if isNavigationHidden { scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift index c943dd0415..6aea40fc22 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift @@ -71,7 +71,6 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV var options: ListViewDeleteAndInsertOptions = [] var maxAnimatedInsertionIndex = -1 - var stationaryItemRange: (Int, Int)? var scrollToItem: ListViewScrollToItem? switch reason { @@ -168,7 +167,17 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV var fromEmptyView = false if let fromView = fromView { - if fromView.filteredEntries.isEmpty || fromView.filter != toView.filter { + var wasSingleHeader = false + if fromView.filteredEntries.count == 1, case .HeaderEntry = fromView.filteredEntries[0] { + wasSingleHeader = true + } + var isSingleHeader = false + if toView.filteredEntries.count == 1, case .HeaderEntry = toView.filteredEntries[0] { + isSingleHeader = true + } + if (wasSingleHeader || isSingleHeader), case .interactiveChanges = reason { + let _ = options.insert(.AnimateInsertion) + } else if fromView.filteredEntries.isEmpty || fromView.filter != toView.filter { options.remove(.AnimateInsertion) options.remove(.AnimateAlpha) fromEmptyView = true @@ -182,7 +191,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV adjustScrollToFirstItem = true } - subscriber.putNext(ChatListNodeViewTransition(chatListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, adjustScrollToFirstItem: adjustScrollToFirstItem)) + subscriber.putNext(ChatListNodeViewTransition(chatListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: nil, adjustScrollToFirstItem: adjustScrollToFirstItem)) subscriber.putCompletion() return EmptyDisposable diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 887ea553de..33ecb3d5e3 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -991,7 +991,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } if let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset, topItemFound { - completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset) + completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset - effectiveInsets.bottom - effectiveInsets.top) bottomItemEdge = max(bottomItemEdge, topItemEdge + completeHeight) } diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 0447d6eb5b..58062e6f8f 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -433,6 +433,12 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true) positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + for controller in strongSelf.container.controllers { + controller.viewWillDisappear(transition.isAnimated) + } completion() }) return positionTransition diff --git a/submodules/MergeLists/Sources/MergeLists.swift b/submodules/MergeLists/Sources/MergeLists.swift index 5b366bba3a..4a12a6da54 100644 --- a/submodules/MergeLists/Sources/MergeLists.swift +++ b/submodules/MergeLists/Sources/MergeLists.swift @@ -215,15 +215,36 @@ public func mergeListsStableWithUpdates(leftList: [T], rightList: [T], isLess for item in rightList { rightStableIds.append(getId(item)) } - if Set(leftStableIds) == Set(rightStableIds) && leftStableIds != rightStableIds { - /*var i = 0 - var j = 0 - while true { + if Set(leftStableIds) == Set(rightStableIds) && leftStableIds != rightStableIds && !allUpdated { + var updatedItems: [(T, AnyHashable)] = [] + for i in 0 ..< leftList.count { if getId(leftList[i]) != getId(rightList[i]) { - + removeIndices.append(i) + } else { + updatedItems.append((leftList[i], getId(leftList[i]))) } - }*/ - print("order changed") + } + var i = 0 + while i < rightList.count { + if updatedItems[i].1 != getId(rightList[i]) { + updatedItems.insert((rightList[i], getId(rightList[i])), at: i) + var previousIndex: Int? + for k in 0 ..< leftList.count { + if getId(leftList[k]) == getId(rightList[i]) { + previousIndex = k + break + } + } + assert(previousIndex != nil) + insertItems.append((i, rightList[i], previousIndex)) + } else { + if !isEqual(updatedItems[i].0, rightList[i]) { + updatedIndices.append((i, rightList[i], i)) + } + } + i += 1 + } + return (removeIndices, insertItems, updatedIndices) } var currentList = leftList diff --git a/submodules/SettingsUI/Sources/DebugController.swift b/submodules/SettingsUI/Sources/DebugController.swift index 2266898b90..1cc961b171 100644 --- a/submodules/SettingsUI/Sources/DebugController.swift +++ b/submodules/SettingsUI/Sources/DebugController.swift @@ -391,6 +391,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { if let context = arguments.context { let _ = (context.account.postbox.transaction { transaction -> Void in transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedPollResults) + unmarkChatListFeaturedFiltersAsSeen(transaction: transaction) }).start() } }) diff --git a/submodules/SettingsUI/Sources/SettingsController.swift b/submodules/SettingsUI/Sources/SettingsController.swift index cbc0798ea0..1cdee54516 100644 --- a/submodules/SettingsUI/Sources/SettingsController.swift +++ b/submodules/SettingsUI/Sources/SettingsController.swift @@ -1263,7 +1263,8 @@ public func settingsController(context: AccountContext, accountManager: AccountM let _ = (contextValue.get() |> deliverOnMainQueue |> take(1)).start(next: { context in - pushControllerImpl?(chatListFilterPresetListController(context: context, updated: { _ in })) + let controller = chatListFilterPresetListController(context: context, mode: .default) + pushControllerImpl?(controller) }) }) diff --git a/submodules/SyncCore/Sources/Namespaces.swift b/submodules/SyncCore/Sources/Namespaces.swift index 17c85e1cf2..a620c1aec3 100644 --- a/submodules/SyncCore/Sources/Namespaces.swift +++ b/submodules/SyncCore/Sources/Namespaces.swift @@ -208,6 +208,7 @@ private enum PreferencesKeyValues: Int32 { case contentSettings = 19 case chatListFilters = 20 case peersNearby = 21 + case chatListFiltersFeaturedState = 22 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -330,6 +331,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.peersNearby.rawValue) return key }() + + public static let chatListFiltersFeaturedState: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.chatListFiltersFeaturedState.rawValue) + return key + }() } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/Account.swift b/submodules/TelegramCore/Sources/Account.swift index be82e24425..4026a65b89 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).start()) + self.managedOperationsDisposable.add(managedChatListFilters(postbox: self.postbox, network: self.network)) let importantBackgroundOperations: [Signal] = [ managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] }, diff --git a/submodules/TelegramCore/Sources/AccountManager.swift b/submodules/TelegramCore/Sources/AccountManager.swift index 855e3aa9e2..075e315698 100644 --- a/submodules/TelegramCore/Sources/AccountManager.swift +++ b/submodules/TelegramCore/Sources/AccountManager.swift @@ -155,6 +155,7 @@ private var declaredEncodables: Void = { declareEncodable(ChatListFiltersState.self, f: { ChatListFiltersState(decoder: $0) }) declareEncodable(PeersNearbyState.self, f: { PeersNearbyState(decoder: $0) }) declareEncodable(TelegramMediaDice.self, f: { TelegramMediaDice(decoder: $0) }) + declareEncodable(ChatListFiltersFeaturedState.self, f: { ChatListFiltersFeaturedState(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/ChatListFiltering.swift b/submodules/TelegramCore/Sources/ChatListFiltering.swift index 0db9b496b6..c673996a19 100644 --- a/submodules/TelegramCore/Sources/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/ChatListFiltering.swift @@ -329,8 +329,11 @@ private func requestChatListFilters(postbox: Postbox, network: Network) -> Signa } } -func managedChatListFilters(postbox: Postbox, network: Network) -> Signal { - return requestChatListFilters(postbox: postbox, network: network) +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() } @@ -343,7 +346,9 @@ func managedChatListFilters(postbox: Postbox, network: Network) -> Signal ignoreValues - } + }).start()) + + return disposables } public func replaceRemoteChatListFilters(account: Account) -> Signal { @@ -461,3 +466,116 @@ public func updateChatListFilterSettingsInteractively(postbox: Postbox, _ f: @es } } +public struct ChatListFeaturedFilter: PostboxCoding, Equatable { + public var title: String + public var description: String + public var data: ChatListFilterData + + fileprivate init( + title: String, + description: String, + data: ChatListFilterData + ) { + self.title = title + self.description = description + self.data = data + } + + public init(decoder: PostboxDecoder) { + self.title = decoder.decodeStringForKey("title", orElse: "") + self.description = decoder.decodeStringForKey("description", orElse: "") + self.data = ChatListFilterData( + categories: ChatListFilterPeerCategories(rawValue: decoder.decodeInt32ForKey("categories", orElse: 0)), + excludeMuted: decoder.decodeInt32ForKey("excludeMuted", orElse: 0) != 0, + excludeRead: decoder.decodeInt32ForKey("excludeRead", orElse: 0) != 0, + excludeArchived: decoder.decodeInt32ForKey("excludeArchived", orElse: 0) != 0, + includePeers: decoder.decodeInt64ArrayForKey("includePeers").map(PeerId.init), + excludePeers: decoder.decodeInt64ArrayForKey("excludePeers").map(PeerId.init) + ) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.title, forKey: "title") + encoder.encodeString(self.description, forKey: "description") + encoder.encodeInt32(self.data.categories.rawValue, forKey: "categories") + encoder.encodeInt32(self.data.excludeMuted ? 1 : 0, forKey: "excludeMuted") + encoder.encodeInt32(self.data.excludeRead ? 1 : 0, forKey: "excludeRead") + encoder.encodeInt32(self.data.excludeArchived ? 1 : 0, forKey: "excludeArchived") + encoder.encodeInt64Array(self.data.includePeers.map { $0.toInt64() }, forKey: "includePeers") + encoder.encodeInt64Array(self.data.excludePeers.map { $0.toInt64() }, forKey: "excludePeers") + } +} + +public struct ChatListFiltersFeaturedState: PreferencesEntry, Equatable { + public var filters: [ChatListFeaturedFilter] + public var isSeen: Bool + + fileprivate init(filters: [ChatListFeaturedFilter], isSeen: Bool) { + self.filters = filters + self.isSeen = isSeen + } + + public init(decoder: PostboxDecoder) { + self.filters = decoder.decodeObjectArrayWithDecoderForKey("filters") + self.isSeen = decoder.decodeInt32ForKey("isSeen", orElse: 0) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeObjectArray(self.filters, forKey: "filters") + encoder.encodeInt32(self.isSeen ? 1 : 0, forKey: "isSeen") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? ChatListFiltersFeaturedState, self == to { + return true + } else { + return false + } + } +} + +public func markChatListFeaturedFiltersAsSeen(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFiltersFeaturedState, { entry in + guard var state = entry as? ChatListFiltersFeaturedState else { + return entry + } + state.isSeen = true + return state + }) + } + |> ignoreValues +} + +public func unmarkChatListFeaturedFiltersAsSeen(transaction: Transaction) { + transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFiltersFeaturedState, { entry in + guard var state = entry as? ChatListFiltersFeaturedState else { + return entry + } + state.isSeen = false + return state + }) +} + +public func updateChatListFeaturedFilters(postbox: Postbox, network: Network) -> Signal { + return network.request(Api.functions.messages.getSuggestedDialogFilters()) + |> `catch` { _ -> Signal<[Api.DialogFilterSuggested], NoError> in + return .single([]) + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFiltersFeaturedState, { entry in + var state = entry as? ChatListFiltersFeaturedState ?? ChatListFiltersFeaturedState(filters: [], isSeen: false) + state.filters = result.map { item -> ChatListFeaturedFilter in + switch item { + case let .dialogFilterSuggested(filter, description): + let parsedFilter = ChatListFilter(apiFilter: filter) + return ChatListFeaturedFilter(title: parsedFilter.title, description: description, data: parsedFilter.data) + } + } + return state + }) + } + |> ignoreValues + } +} diff --git a/submodules/TelegramUI/Resources/Animations/ChatListFilterEmpty.tgs b/submodules/TelegramUI/Resources/Animations/ChatListFilterEmpty.tgs index ee8755f78a..2dd3799596 100644 Binary files a/submodules/TelegramUI/Resources/Animations/ChatListFilterEmpty.tgs and b/submodules/TelegramUI/Resources/Animations/ChatListFilterEmpty.tgs differ diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 067b1a4570..bd4824836f 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -77,7 +77,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: backgroundFrame.height), size: arrowSize)) } - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: contentVerticalInset), size: textSize)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize)) transition.updateFrame(node: self.animatedStickerNode, frame: CGRect(origin: CGPoint(x: contentInset - animationInset, y: floor((backgroundHeight - animationSize.height) / 2.0) - animationInset), size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0))) self.animatedStickerNode.updateLayout(size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0)) @@ -90,7 +90,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { eventIsPresses = event.type == .presses } if event.type == .touches || eventIsPresses { - self.requestDismiss() + //self.requestDismiss() return nil } } @@ -144,6 +144,10 @@ public final class TooltipScreen: ViewController { super.viewDidAppear(animated) self.controllerNode.animateIn() + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0, execute: { [weak self] in + self?.dismiss() + }) } override public func loadDisplayNode() {