Filter improvements

This commit is contained in:
Ali 2020-02-28 20:22:39 +04:00
parent 8341247b5d
commit 5e724b92ea
39 changed files with 5519 additions and 4384 deletions

View File

@ -5366,3 +5366,5 @@ Any member of this group will be able to see messages in the channel.";
"Stats.ViewsBySourceTitle" = "VIEWS BY SOURCE";
"Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE";
"Stats.LanguagesTitle" = "LANGUAGES";
"ChatListFilter.AddChatsTitle" = "Add Chats";

View File

@ -7,6 +7,7 @@ public enum ContactMultiselectionControllerMode {
case groupCreation
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
case channelCreation
case chatSelection
}
public enum ContactListFilter {

View File

@ -23,19 +23,26 @@ import LocalizedPeerData
import TelegramIntents
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if listNode.scroller.isDragging {
return false
}
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
let scrollToItem: ListViewScrollToItem
let targetProgress: CGFloat
let offset: CGFloat
if searchNode.expansionProgress < 0.6 {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: true, curve: .Default(duration: nil), directionHint: .Up)
targetProgress = 0.0
offset = navigationBarSearchContentHeight
} else {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up)
targetProgress = 1.0
offset = 0.0
}
searchNode.updateExpansionProgress(targetProgress, animated: true)
//searchNode.updateExpansionProgress(targetProgress, animated: true)
listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
//listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
listNode.scrollToOffsetFromTop(offset)
return true
} else if searchNode.expansionProgress == 1.0 {
var sortItemNode: ListViewItemNode?
@ -138,13 +145,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
private var searchContentNode: NavigationBarSearchContentNode?
private let tabContainerNode: ChatListFilterTabContainerNode
private var tabContainerData: ([ChatListFilterTabEntry], ChatListFilterTabEntryId)?
private let chatListFilterValue = Promise<ChatListFilter?>()
private var tabContainerData: [ChatListFilterTabEntry]?
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
if self.isNodeLoaded {
self.chatListDisplayNode.chatListNode.updateSelectedChatLocation(data as? ChatLocation, progress: progress, transition: transition)
self.chatListDisplayNode.containerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition)
}
}
@ -232,12 +237,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
if strongSelf.chatListDisplayNode.searchDisplayController != nil {
strongSelf.deactivateSearch(animated: true)
} else {
switch strongSelf.chatListDisplayNode.chatListNode.visibleContentOffset() {
switch strongSelf.chatListDisplayNode.containerNode.currentItemNode.visibleContentOffset() {
case .none, .unknown:
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.chatListDisplayNode.chatListNode.scrollToPosition(.top)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.scrollToPosition(.top)
case let .known(offset):
if offset <= navigationBarSearchContentHeight + 1.0 {
strongSelf.tabContainerNode.tabSelected?(.all)
@ -245,24 +250,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.chatListDisplayNode.chatListNode.scrollToPosition(.top)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.scrollToPosition(.top)
}
}
}
}
self.longTapWithTabBar = { [weak self] in
guard let strongSelf = self else {
return
}
if strongSelf.chatListDisplayNode.searchDisplayController != nil {
strongSelf.deactivateSearch(animated: true)
} else {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.chatListDisplayNode.chatListNode.scrollToPosition(.auto)
}
}
let hasProxy = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.proxySettings])
|> map { sharedData -> (Bool, Bool) in
@ -287,22 +279,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
context.account.networkState,
hasProxy,
passcode,
self.chatListDisplayNode.chatListNode.state
self.chatListDisplayNode.containerNode.currentItemState
).start(next: { [weak self] networkState, proxy, passcode, state in
if let strongSelf = self {
let defaultTitle: String
if strongSelf.groupId == .root {
if let chatListFilter = strongSelf.filter {
let title: String = chatListFilter.title ?? strongSelf.presentationData.strings.DialogList_Title
defaultTitle = title
} else {
defaultTitle = strongSelf.presentationData.strings.DialogList_Title
}
defaultTitle = strongSelf.presentationData.strings.DialogList_Title
} else {
defaultTitle = strongSelf.presentationData.strings.ChatList_ArchivedChatsTitle
}
if state.editing {
if strongSelf.groupId == .root && strongSelf.filter == nil {
if strongSelf.groupId == .root {
strongSelf.navigationItem.rightBarButtonItem = nil
}
@ -312,11 +299,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
var isRoot = false
if case .root = strongSelf.groupId {
isRoot = true
if strongSelf.filter == nil {
let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed))
rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose
strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem
}
let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed))
rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose
strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem
}
let (hasProxy, connectsViaProxy) = proxy
@ -420,30 +405,39 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
if self.filter == nil {
self.chatListDisplayNode.containerNode.currentItemFilterUpdated = { [weak self] filter, fraction, transition in
guard let strongSelf = self else {
return
}
guard let layout = strongSelf.validLayout else {
return
}
guard let tabContainerData = strongSelf.tabContainerData else {
return
}
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData, selectedFilter: filter, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition)
}
let preferencesKey: PostboxViewKey = .preferences(keys: Set([
ApplicationSpecificPreferencesKeys.chatListFilterSettings
]))
let filterItems = chatListFilterItems(context: context)
|> map { totalCount, items -> [ChatListFilterTabEntry] in
var result: [ChatListFilterTabEntry] = []
result.append(.all(unreadCount: totalCount))
for (filter, unreadCount) in items {
result.append(.filter(id: filter.id, text: filter.title ?? "", unreadCount: unreadCount))
}
return result
}
|> distinctUntilChanged
self.filterDisposable = (combineLatest(queue: .mainQueue(),
context.account.postbox.combinedView(keys: [
preferencesKey
]),
filterItems,
self.chatListFilterValue.get() |> map { $0?.id } |> distinctUntilChanged
filterItems
)
|> deliverOnMainQueue).start(next: { [weak self] combinedView, filterItems, selectedFilter in
|> deliverOnMainQueue).start(next: { [weak self] combinedView, countAndFilterItems in
guard let strongSelf = self else {
return
}
let (totalCount, items) = countAndFilterItems
var filterItems: [ChatListFilterTabEntry] = []
filterItems.append(.all(unreadCount: totalCount))
for (filter, unreadCount) in items {
filterItems.append(.filter(id: filter.id, text: filter.title, unreadCount: unreadCount))
}
var filterSettings: ChatListFilterSettings = .default
if let preferencesView = combinedView.views[preferencesKey] as? PreferencesView {
if let value = preferencesView.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings] as? ChatListFilterSettings {
@ -458,12 +452,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
var wasEmpty = false
if let tabContainerData = strongSelf.tabContainerData {
wasEmpty = tabContainerData.0.count <= 1
wasEmpty = tabContainerData.count <= 1
} else {
wasEmpty = true
}
let selectedEntryId: ChatListFilterTabEntryId = selectedFilter.flatMap { .filter($0) } ?? .all
strongSelf.tabContainerData = (resolvedItems, selectedEntryId)
let selectedEntryId = strongSelf.chatListDisplayNode.containerNode.currentItemFilter
strongSelf.tabContainerData = resolvedItems
var availableFilters: [ChatListContainerNodeFilter] = []
availableFilters.append(.all)
for item in items {
availableFilters.append(.filter(item.0))
}
strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters)
let isEmpty = resolvedItems.count <= 1
@ -479,7 +479,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
(strongSelf.parent as? TabBarController)?.updateLayout()
} else {
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: NavigationBar.defaultSecondaryContentHeight), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring))
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
@ -497,7 +497,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
let previousFilter = strongSelf.chatListDisplayNode.chatListNode.chatListFilter
let previousFilter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter
let updatedFilter: ChatListFilter?
switch id {
case .all:
@ -518,26 +518,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
updatedFilter = nil
}
}
if previousFilter?.id != updatedFilter?.id {
var paneSwitchAnimationDirection: ChatListNodePaneSwitchAnimationDirection?
if let previousId = previousFilter?.id, let updatedId = updatedFilter?.id, let previousIndex = filters.index(where: { $0.id == previousId }), let updatedIndex = filters.index(where: { $0.id == updatedId }) {
if previousIndex > updatedIndex {
paneSwitchAnimationDirection = .right
} else {
paneSwitchAnimationDirection = .left
}
} else if (previousFilter != nil) != (updatedFilter != nil) {
if previousFilter != nil {
paneSwitchAnimationDirection = .right
} else {
paneSwitchAnimationDirection = .left
}
}
if let direction = paneSwitchAnimationDirection {
strongSelf.chatListDisplayNode.chatListNode.paneSwitchAnimation = (direction, .animated(duration: 0.4, curve: .spring))
}
}
strongSelf.chatListDisplayNode.chatListNode.updateFilter(updatedFilter)
strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: updatedFilter.flatMap { .filter($0.id) } ?? .all)
})
}
@ -549,38 +530,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Edit, icon: { _ in
return nil
}, action: { c, f in
c.dismiss(completion: {
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
}
|> deliverOnMainQueue).start(next: { presetList in
guard let strongSelf = self else {
return
}
var found = false
for filter in presetList {
if filter.id == id {
strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in }))
f(.dismissWithoutContent)
found = true
break
}
}
})
})
})))
if let chatListFilter = strongSelf.chatListDisplayNode.chatListNode.chatListFilter, chatListFilter.includePeers.count < 100 {
//TODO:localization
items.append(.action(ContextMenuActionItem(text: "Add Chats", icon: { _ in
return nil
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [ChatListFilter] in
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default
return settings.filters
}
|> deliverOnMainQueue).start(next: { [weak self] filters in
guard let strongSelf = self else {
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Edit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
}, action: { c, f in
c.dismiss(completion: {
guard let strongSelf = self else {
@ -597,7 +557,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
var found = false
for filter in presetList {
if filter.id == id {
strongSelf.push(chatListFilterAddChatsController(context: strongSelf.context, filter: filter))
strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in }))
f(.dismissWithoutContent)
found = true
break
@ -606,10 +566,74 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
})
})
})))
}
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture)
strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
if let filter = filters.first(where: { $0.id == id }), filter.data.includePeers.count < 100 {
//TODO:localization
items.append(.action(ContextMenuActionItem(text: "Add Chats", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { c, f in
c.dismiss(completion: {
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
}
|> deliverOnMainQueue).start(next: { presetList in
guard let strongSelf = self else {
return
}
var found = false
for filter in presetList {
if filter.id == id {
strongSelf.push(chatListFilterAddChatsController(context: strongSelf.context, filter: filter))
f(.dismissWithoutContent)
found = true
break
}
}
})
})
})))
items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { c, f in
c.dismiss(completion: {
guard let strongSelf = self else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = updateChatListFilterSettingsInteractively(postbox: strongSelf.context.account.postbox, { settings in
var settings = settings
settings.filters = settings.filters.filter({ $0.id != id })
return settings
}).start()
let _ = replaceRemoteChatListFilters(account: strongSelf.context.account).start()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.present(actionSheet, in: .window(.root))
})
})))
}
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture)
strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
})
}
}
@ -642,11 +666,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.DialogList_SearchLabel)
var editing = false
self.chatListDisplayNode.chatListNode.updateState { state in
editing = state.editing
return state
}
let editing = self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing
let editItem: UIBarButtonItem
if editing {
editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
@ -676,7 +696,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
override public func loadDisplayNode() {
self.displayNode = ChatListControllerNode(context: self.context, groupId: self.groupId, filter: self.filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, controller: self)
self.chatListFilterValue.set(self.chatListDisplayNode.chatListNode.appliedChatListFilterSignal)
self.chatListDisplayNode.navigationBar = self.navigationBar
@ -684,37 +703,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self?.deactivateSearch(animated: true)
}
self.chatListDisplayNode.chatListNode.activateSearch = { [weak self] in
self.chatListDisplayNode.containerNode.activateSearch = { [weak self] in
self?.activateSearch()
}
self.chatListDisplayNode.chatListNode.presentAlert = { [weak self] text in
self.chatListDisplayNode.containerNode.presentAlert = { [weak self] text in
if let strongSelf = self {
self?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}
self.chatListDisplayNode.chatListNode.present = { [weak self] c in
self.chatListDisplayNode.containerNode.present = { [weak self] c in
if let strongSelf = self {
self?.present(c, in: .window(.root))
}
}
self.chatListDisplayNode.chatListNode.toggleArchivedFolderHiddenByDefault = { [weak self] in
self.chatListDisplayNode.containerNode.toggleArchivedFolderHiddenByDefault = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.toggleArchivedFolderHiddenByDefault()
}
self.chatListDisplayNode.chatListNode.deletePeerChat = { [weak self] peerId in
self.chatListDisplayNode.containerNode.deletePeerChat = { [weak self] peerId in
guard let strongSelf = self else {
return
}
strongSelf.deletePeerChat(peerId: peerId)
}
self.chatListDisplayNode.chatListNode.peerSelected = { [weak self] peerId, animated, isAd in
self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, animated, isAd in
if let strongSelf = self {
if let navigationController = strongSelf.navigationController as? NavigationController {
if isAd {
@ -738,37 +757,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
scrollToEndIfExists = true
}
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), scrollToEndIfExists: scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] in
self?.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), scrollToEndIfExists: scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] in
self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
}))
}
}
}
self.chatListDisplayNode.chatListNode.groupSelected = { [weak self] groupId in
self.chatListDisplayNode.containerNode.groupSelected = { [weak self] groupId in
if let strongSelf = self {
if let navigationController = strongSelf.navigationController as? NavigationController {
let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupId, controlsHistoryPreload: false, enableDebugActions: false)
chatListController.navigationPresentation = .master
navigationController.pushViewController(chatListController)
strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
}
}
}
self.chatListDisplayNode.chatListNode.updatePeerGrouping = { [weak self] peerId, group in
self.chatListDisplayNode.containerNode.updatePeerGrouping = { [weak self] peerId, group in
guard let strongSelf = self else {
return
}
if group {
strongSelf.archiveChats(peerIds: [peerId])
} else {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerId)
let _ = updatePeerGroupIdInteractively(postbox: strongSelf.context.account.postbox, peerId: peerId, groupId: group ? Namespaces.PeerGroup.archive : .root).start(completed: {
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
})
}
}
@ -786,7 +805,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeerId), subject: .message(messageId), purposefulAction: {
self?.deactivateSearch(animated: false)
}, scrollToEndIfExists: scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : []))
strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
}
}
}))
@ -815,7 +834,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), purposefulAction: { [weak self] in
self?.deactivateSearch(animated: false)
}, scrollToEndIfExists: scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : []))
strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
}
}
}))
@ -867,7 +886,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
navigationController.filterController(strongSelf, animated: true)
}
self.chatListDisplayNode.chatListNode.contentOffsetChanged = { [weak self] offset in
self.chatListDisplayNode.containerNode.contentOffsetChanged = { [weak self] offset in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode, let validLayout = strongSelf.validLayout {
var offset = offset
if validLayout.inVoiceOver {
@ -877,7 +896,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
}
self.chatListDisplayNode.chatListNode.contentScrollingEnded = { [weak self] listView in
self.chatListDisplayNode.containerNode.contentScrollingEnded = { [weak self] listView in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
return fixListNodeScrolling(listView, searchNode: searchContentNode)
} else {
@ -897,7 +916,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
if let filter = strongSelf.chatListDisplayNode.chatListNode.chatListFilter {
if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter {
strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in }))
} else {
strongSelf.composePressed()
@ -908,7 +927,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self?.toolbarActionSelected(action: action)
}
self.chatListDisplayNode.chatListNode.activateChatPreview = { [weak self] item, node, gesture in
self.chatListDisplayNode.containerNode.activateChatPreview = { [weak self] item, node, gesture in
guard let strongSelf = self else {
gesture?.cancel()
return
@ -940,7 +959,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
let context = self.context
let peerIdsAndOptions: Signal<(ChatListSelectionOptions, Set<PeerId>)?, NoError> = self.chatListDisplayNode.chatListNode.state
let peerIdsAndOptions: Signal<(ChatListSelectionOptions, Set<PeerId>)?, NoError> = self.chatListDisplayNode.containerNode.currentItemState
|> map { state -> Set<PeerId>? in
if !state.editing {
return nil
@ -988,7 +1007,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
}
}
toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: strongSelf.chatListDisplayNode.chatListNode.chatListFilter != nil ? nil : ToolbarAction(title: presentationData.strings.ChatList_ArchiveAction, isEnabled: archiveEnabled))
toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter != nil ? nil : ToolbarAction(title: presentationData.strings.ChatList_ArchiveAction, isEnabled: archiveEnabled))
}
} else {
if let (options, peerIds) = peerIdsAndOptions {
@ -1006,7 +1025,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
strongSelf.setToolbar(toolbar, transition: .animated(duration: 0.3, curve: .easeInOut))
}))
self.ready.set(self.chatListDisplayNode.chatListNode.ready)
self.ready.set(self.chatListDisplayNode.containerNode.ready)
self.displayNodeDidLoad()
}
@ -1105,7 +1124,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}))
}
self.chatListDisplayNode.chatListNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in
self.chatListDisplayNode.containerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in
guard let strongSelf = self else {
return
}
@ -1145,7 +1164,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.deactivateSearch(animated: false)
}
self.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
self.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -1161,12 +1180,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
tabContainerOffset += 44.0 + 44.0 + 44.0
}
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.visualNavigationInsetHeight - self.additionalHeight - NavigationBar.defaultSecondaryContentHeight + tabContainerOffset), size: CGSize(width: layout.size.width, height: NavigationBar.defaultSecondaryContentHeight)))
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: NavigationBar.defaultSecondaryContentHeight), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.tabContainerData?.1, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.visualNavigationInsetHeight - self.additionalHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0)))
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver {
searchContentNode.updateListVisibleContentOffset(.known(0.0))
self.chatListDisplayNode.chatListNode.scrollToPosition(.top)
self.chatListDisplayNode.scrollToTop()
}
self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, visualNavigationHeight: self.visualNavigationInsetHeight, cleanNavigationBarHeight: self.cleanNavigationHeight, transition: transition)
@ -1188,7 +1207,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
self.searchContentNode?.setIsEnabled(false, animated: true)
self.chatListDisplayNode.chatListNode.updateState { state in
self.chatListDisplayNode.containerNode.updateState { state in
var state = state
state.editing = true
state.peerIdWithRevealedOptions = nil
@ -1206,7 +1225,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
(self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(nil, transition: .animated(duration: 0.4, curve: .spring))
self.searchContentNode?.setIsEnabled(true, animated: true)
self.chatListDisplayNode.chatListNode.updateState { state in
self.chatListDisplayNode.containerNode.updateState { state in
var state = state
state.editing = false
state.peerIdWithRevealedOptions = nil
@ -1217,7 +1236,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
public func activateSearch() {
if self.displayNavigationBar {
let _ = (self.chatListDisplayNode.chatListNode.contentsReady
let _ = (self.chatListDisplayNode.containerNode.currentItemNode.contentsReady
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else {
@ -1303,10 +1322,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return nil
}
let listLocation = self.view.convert(location, to: self.chatListDisplayNode.chatListNode.view)
let listLocation = self.view.convert(location, to: self.chatListDisplayNode.containerNode.currentItemNode.view)
var selectedNode: ChatListItemNode?
self.chatListDisplayNode.chatListNode.forEachItemNode { itemNode in
self.chatListDisplayNode.containerNode.currentItemNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatListItemNode, itemNode.frame.contains(listLocation), !itemNode.isDisplayingRevealedOptions {
selectedNode = itemNode
}
@ -1346,12 +1365,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
chatController.updatePresentationMode(.standard(previewing: false))
if let navigationController = self.navigationController as? NavigationController {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: chatController, context: self.context, chatLocation: chatController.chatLocation, animated: false))
self.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
self.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
}
} else if let chatListController = viewControllerToCommit as? ChatListController {
if let navigationController = self.navigationController as? NavigationController {
navigationController.pushViewController(chatListController, animated: false, completion: {})
self.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
self.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
}
}
}
@ -1373,22 +1392,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
let inputShortcuts: [KeyShortcut] = [
KeyShortcut(title: strings.KeyCommand_JumpToPreviousChat, input: UIKeyCommand.inputUpArrow, modifiers: [.alternate], action: { [weak self] in
if let strongSelf = self {
strongSelf.chatListDisplayNode.chatListNode.selectChat(.previous(unread: false))
strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.previous(unread: false))
}
}),
KeyShortcut(title: strings.KeyCommand_JumpToNextChat, input: UIKeyCommand.inputDownArrow, modifiers: [.alternate], action: { [weak self] in
if let strongSelf = self {
strongSelf.chatListDisplayNode.chatListNode.selectChat(.next(unread: false))
strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.next(unread: false))
}
}),
KeyShortcut(title: strings.KeyCommand_JumpToPreviousUnreadChat, input: UIKeyCommand.inputUpArrow, modifiers: [.alternate, .shift], action: { [weak self] in
if let strongSelf = self {
strongSelf.chatListDisplayNode.chatListNode.selectChat(.previous(unread: true))
strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.previous(unread: true))
}
}),
KeyShortcut(title: strings.KeyCommand_JumpToNextUnreadChat, input: UIKeyCommand.inputDownArrow, modifiers: [.alternate, .shift], action: { [weak self] in
if let strongSelf = self {
strongSelf.chatListDisplayNode.chatListNode.selectChat(.next(unread: true))
strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.next(unread: true))
}
}),
KeyShortcut(title: strings.KeyCommand_NewMessage, input: "N", modifiers: [.command], action: { [weak self] in
@ -1403,9 +1422,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
let openChat: (Int) -> Void = { [weak self] index in
if let strongSelf = self {
if index == 0 {
strongSelf.chatListDisplayNode.chatListNode.selectChat(.peerId(strongSelf.context.account.peerId))
strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.peerId(strongSelf.context.account.peerId))
} else {
strongSelf.chatListDisplayNode.chatListNode.selectChat(.index(index - 1))
strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.index(index - 1))
}
}
}
@ -1420,7 +1439,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
override public func toolbarActionSelected(action: ToolbarActionOption) {
let peerIds = self.chatListDisplayNode.chatListNode.currentState.selectedPeerIds
let peerIds = self.chatListDisplayNode.containerNode.currentItemNode.currentState.selectedPeerIds
if case .left = action {
let signal: Signal<Void, NoError>
let context = self.context
@ -1433,7 +1452,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} else {
let groupId = self.groupId
signal = self.context.account.postbox.transaction { transaction -> Void in
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: self.chatListDisplayNode.chatListNode.chatListFilter.flatMap(chatListFilterPredicate))
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: (self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data).flatMap(chatListFilterPredicate))
}
}
let _ = (signal
@ -1450,7 +1469,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return
}
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state
for peerId in peerIds {
state.pendingRemovalPeerIds.insert(peerId)
@ -1494,15 +1513,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|> deliverOnMainQueue).start()
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds.first!)
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!)
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state
for peerId in peerIds {
state.pendingRemovalPeerIds.remove(peerId)
}
return state
})
self?.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds.first!)
self?.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!)
return true
}
return false
@ -1526,7 +1545,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.archiveChats(peerIds: Array(peerIds))
} else {
if !peerIds.isEmpty {
self.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds.first!)
self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!)
let _ = (self.context.account.postbox.transaction { transaction -> Void in
for peerId in peerIds {
updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root)
@ -1536,7 +1555,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
strongSelf.donePressed()
})
}
@ -1559,7 +1578,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.chatListNode.updateState { state in
strongSelf.chatListDisplayNode.containerNode.updateState { state in
var state = state
if value {
state.archiveShouldBeTemporaryRevealed = false
@ -1677,7 +1696,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state
state.pendingClearHistoryPeerIds.insert(peer.peerId)
return state
@ -1698,7 +1717,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state
state.pendingClearHistoryPeerIds.remove(peer.peerId)
return state
@ -1706,7 +1725,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
})
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state
state.pendingClearHistoryPeerIds.remove(peer.peerId)
return state
@ -1870,7 +1889,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return
}
let postbox = self.context.account.postbox
self.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds[0])
self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds[0])
let _ = (ApplicationSpecificNotice.incrementArchiveChatTips(accountManager: self.context.sharedContext.accountManager, count: 1)
|> deliverOnMainQueue).start(next: { [weak self] previousHintCount in
let _ = (postbox.transaction { transaction -> Void in
@ -1882,7 +1901,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
for peerId in peerIds {
deleteSendMessageIntents(peerId: peerId)
@ -1893,7 +1912,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return false
}
if value == .undo {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds[0])
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds[0])
let _ = (postbox.transaction { transaction -> Void in
for peerId in peerIds {
updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root)
@ -1903,7 +1922,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
})
return true
} else {
@ -1949,13 +1968,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
let peerId = peer.peerId
self.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId)
self.chatListDisplayNode.chatListNode.updateState({ state in
self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerId)
self.chatListDisplayNode.containerNode.updateState({ state in
var state = state
state.pendingRemovalPeerIds.insert(peer.peerId)
return state
})
self.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil)
self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
let statusText: String
if let channel = chatPeer as? TelegramChannel {
if deleteGloballyIfPossible {
@ -1999,7 +2018,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return false
}
if value == .commit {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerId)
if let channel = chatPeer as? TelegramChannel {
strongSelf.context.peerChannelMemberCategoriesContextsManager.externallyRemoved(peerId: channel.id, memberId: strongSelf.context.account.peerId)
}
@ -2007,25 +2026,25 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state
state.pendingRemovalPeerIds.remove(peer.peerId)
return state
})
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
deleteSendMessageIntents(peerId: peerId)
})
completion()
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId)
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerId)
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state
state.pendingRemovalPeerIds.remove(peer.peerId)
return state
})
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil)
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
return true
}
return false
@ -2054,35 +2073,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}))
}
public func presentTabBarPreviewingController(sourceNodes: [ASDisplayNode]) {
if self.isNodeLoaded {
let _ = (self.context.account.postbox.transaction { transaction -> [ChatListFilter] in
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default
return settings.filters
}
|> deliverOnMainQueue).start(next: { [weak self] presetList in
guard let strongSelf = self else {
return
}
let controller = TabBarChatListFilterController(context: strongSelf.context, sourceNodes: sourceNodes, presetList: presetList, currentPreset: strongSelf.chatListDisplayNode.chatListNode.chatListFilter, setup: {
guard let strongSelf = self else {
return
}
strongSelf.push(chatListFilterPresetListController(context: strongSelf.context, updated: { _ in
}))
}, updatePreset: { value in
guard let strongSelf = self else {
return
}
if let value = value {
strongSelf.tabContainerNode.tabSelected?(.filter(value.id))
}
})
strongSelf.context.sharedContext.mainWindow?.present(controller, on: .root)
})
}
}
override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
let _ = (combineLatest(queue: .mainQueue(),
self.context.account.postbox.transaction { transaction -> [ChatListFilter] in
@ -2120,6 +2110,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
})
})))
if strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter != nil {
items.append(.action(ContextMenuActionItem(text: "All Chats", icon: { theme in
return nil
}, action: { c, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
strongSelf.tabContainerNode.tabSelected?(.all)
})))
}
if !presetList.isEmpty {
items.append(.separator)
@ -2135,25 +2137,25 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
case privateChats
}
let filterType: ChatListFilterType
if preset.includePeers.isEmpty {
if preset.categories == .all {
if preset.excludeRead {
if preset.data.includePeers.isEmpty {
if preset.data.categories == .all {
if preset.data.excludeRead {
filterType = .unread
} else if preset.excludeMuted {
} else if preset.data.excludeMuted {
filterType = .unmuted
} else {
filterType = .generic
}
} else {
if preset.categories == .channels {
if preset.data.categories == .channels {
filterType = .channels
} else if preset.categories.isSubset(of: [.publicGroups, .privateGroups]) {
} else if preset.data.categories.isSubset(of: [.publicGroups, .privateGroups]) {
filterType = .groups
} else if preset.categories == .bots {
} else if preset.data.categories == .bots {
filterType = .bots
} else if preset.categories == .secretChats {
} else if preset.data.categories == .secretChats {
filterType = .secretChats
} else if preset.categories == .privateChats {
} else if preset.data.categories == .privateChats {
filterType = .privateChats
} else {
filterType = .generic
@ -2203,6 +2205,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
})
}
override public func tabBarItemSwipeAction(direction: TabBarItemSwipeDirection) {
guard let entries = self.tabContainerData, var index = entries.index(where: { $0.id == self.chatListDisplayNode.containerNode.currentItemFilter }) else {
return
}
switch direction {
case .right:
if index == 0 {
index = entries.count - 1
} else {
index -= 1
}
case .left:
if index == entries.count - 1 {
index = 0
} else {
index += 1
}
}
self.tabContainerNode.tabSelected?(entries[index].id)
}
}
private final class ChatListTabBarContextExtractedContentSource: ContextExtractedContentSource {

View File

@ -26,19 +26,579 @@ private final class ChatListControllerNodeView: UITracingLayerView, PreviewingHo
weak var controller: ChatListControllerImpl?
}
private struct TestItem: Comparable, Identifiable {
var value: Int
var version: Int
enum ChatListContainerNodeFilter: Equatable {
case all
case filter(ChatListFilter)
var stableId: Int {
return self.value
var id: ChatListFilterTabEntryId {
switch self {
case .all:
return .all
case let .filter(filter):
return .filter(filter.id)
}
}
static func <(lhs: TestItem, rhs: TestItem) -> Bool {
if lhs.version != rhs.version {
return lhs.version < rhs.version
var filter: ChatListFilter? {
switch self {
case .all:
return nil
case let .filter(filter):
return filter
}
}
}
private final class ChatListContainerItemNode: ASDisplayNode {
private var presentationData: PresentationData
private let becameEmpty: (ChatListFilter?) -> Void
private let emptyAction: (ChatListFilter?) -> Void
private var emptyNode: ChatListEmptyNode?
let listNode: ChatListNode
private var validLayout: (CGSize, UIEdgeInsets, CGFloat)?
init(context: AccountContext, groupId: PeerGroupId, filter: ChatListFilter?, previewing: Bool, presentationData: PresentationData, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void) {
self.presentationData = presentationData
self.becameEmpty = becameEmpty
self.emptyAction = emptyAction
self.listNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, controlsHistoryPreload: false, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
super.init()
self.addSubnode(self.listNode)
self.listNode.isEmptyUpdated = { [weak self] isEmptyState, _, _, transition in
guard let strongSelf = self else {
return
}
switch isEmptyState {
case let .empty(isLoading):
if let currentNode = strongSelf.emptyNode {
currentNode.updateIsLoading(isLoading)
} else {
let emptyNode = ChatListEmptyNode(isFilter: filter != nil, isLoading: isLoading, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, action: {
self?.emptyAction(filter)
})
strongSelf.emptyNode = emptyNode
strongSelf.addSubnode(emptyNode)
if let (size, insets, _) = strongSelf.validLayout {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
emptyNode.frame = emptyNodeFrame
emptyNode.updateLayout(size: emptyNodeFrame.size, transition: .immediate)
}
}
strongSelf.becameEmpty(filter)
case .notEmpty:
if let emptyNode = strongSelf.emptyNode {
strongSelf.emptyNode = nil
transition.updateAlpha(node: emptyNode, alpha: 0.0, completion: { [weak emptyNode] _ in
emptyNode?.removeFromSupernode()
})
}
}
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.listNode.updateThemeAndStrings(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
self.emptyNode?.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, visualNavigationHeight)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: 0.0, curve: .Default(duration: 0.0))
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
self.listNode.visualInsets = UIEdgeInsets(top: visualNavigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
self.listNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets)
if let emptyNode = self.emptyNode {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
transition.updateFrame(node: emptyNode, frame: emptyNodeFrame)
emptyNode.updateLayout(size: emptyNodeFrame.size, transition: transition)
}
}
}
final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
private let context: AccountContext
private let groupId: PeerGroupId
private let previewing: Bool
private let filterBecameEmpty: (ChatListFilter?) -> Void
private let filterEmptyAction: (ChatListFilter?) -> Void
private var presentationData: PresentationData
private var itemNodes: [ChatListFilterTabEntryId: ChatListContainerItemNode] = [:]
private var pendingItemNode: (ChatListFilterTabEntryId, ChatListContainerItemNode, Disposable)?
private var availableFilters: [ChatListContainerNodeFilter] = [.all]
private var selectedId: ChatListFilterTabEntryId
private(set) var transitionFraction: CGFloat = 0.0
private var disableItemNodeOperationsWhileAnimating: Bool = false
private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat)?
private let _ready = Promise<Bool>()
var ready: Signal<Bool, NoError> {
return _ready.get()
}
private var currentItemNodeValue: ChatListContainerItemNode?
var currentItemNode: ChatListNode {
return self.currentItemNodeValue!.listNode
}
private let currentItemStateValue = Promise<ChatListNodeState>()
var currentItemState: Signal<ChatListNodeState, NoError> {
return self.currentItemStateValue.get()
}
var currentItemFilterUpdated: ((ChatListFilterTabEntryId, CGFloat, ContainedViewLayoutTransition) -> Void)?
var currentItemFilter: ChatListFilterTabEntryId {
return self.currentItemNode.chatListFilter.flatMap { .filter($0.id) } ?? .all
}
private func applyItemNodeAsCurrent(id: ChatListFilterTabEntryId, itemNode: ChatListContainerItemNode) {
if let previousItemNode = self.currentItemNodeValue {
previousItemNode.listNode.activateSearch = nil
previousItemNode.listNode.presentAlert = nil
previousItemNode.listNode.present = nil
previousItemNode.listNode.toggleArchivedFolderHiddenByDefault = nil
previousItemNode.listNode.deletePeerChat = nil
previousItemNode.listNode.peerSelected = nil
previousItemNode.listNode.groupSelected = nil
previousItemNode.listNode.updatePeerGrouping = nil
previousItemNode.listNode.contentOffsetChanged = nil
previousItemNode.listNode.contentScrollingEnded = nil
previousItemNode.listNode.activateChatPreview = nil
previousItemNode.listNode.addedVisibleChatsWithPeerIds = nil
previousItemNode.accessibilityElementsHidden = true
}
self.currentItemNodeValue = itemNode
itemNode.accessibilityElementsHidden = false
itemNode.listNode.activateSearch = { [weak self] in
self?.activateSearch?()
}
itemNode.listNode.presentAlert = { [weak self] text in
self?.presentAlert?(text)
}
itemNode.listNode.present = { [weak self] c in
self?.present?(c)
}
itemNode.listNode.toggleArchivedFolderHiddenByDefault = { [weak self] in
self?.toggleArchivedFolderHiddenByDefault?()
}
itemNode.listNode.deletePeerChat = { [weak self] peerId in
self?.deletePeerChat?(peerId)
}
itemNode.listNode.peerSelected = { [weak self] peerId, a, b in
self?.peerSelected?(peerId, a, b)
}
itemNode.listNode.groupSelected = { [weak self] groupId in
self?.groupSelected?(groupId)
}
itemNode.listNode.updatePeerGrouping = { [weak self] peerId, group in
self?.updatePeerGrouping?(peerId, group)
}
itemNode.listNode.contentOffsetChanged = { [weak self] offset in
self?.contentOffsetChanged?(offset)
}
itemNode.listNode.contentScrollingEnded = { [weak self] listView in
return self?.contentScrollingEnded?(listView) ?? false
}
itemNode.listNode.activateChatPreview = { [weak self] item, sourceNode, gesture in
self?.activateChatPreview?(item, sourceNode, gesture)
}
itemNode.listNode.addedVisibleChatsWithPeerIds = { [weak self] ids in
self?.addedVisibleChatsWithPeerIds?(ids)
}
self.currentItemStateValue.set(itemNode.listNode.state)
}
var activateSearch: (() -> Void)?
var presentAlert: ((String) -> Void)?
var present: ((ViewController) -> Void)?
var toggleArchivedFolderHiddenByDefault: (() -> Void)?
var deletePeerChat: ((PeerId) -> Void)?
var peerSelected: ((Peer, Bool, Bool) -> Void)?
var groupSelected: ((PeerGroupId) -> Void)?
var updatePeerGrouping: ((PeerId, Bool) -> Void)?
var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
var contentScrollingEnded: ((ListView) -> Bool)?
var activateChatPreview: ((ChatListItem, ASDisplayNode, ContextGesture?) -> Void)?
var addedVisibleChatsWithPeerIds: (([PeerId]) -> Void)?
init(context: AccountContext, groupId: PeerGroupId, previewing: Bool, presentationData: PresentationData, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void) {
self.context = context
self.groupId = groupId
self.previewing = previewing
self.filterBecameEmpty = filterBecameEmpty
self.filterEmptyAction = filterEmptyAction
self.presentationData = presentationData
self.selectedId = .all
super.init()
let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: nil, previewing: self.previewing, presentationData: presentationData, becameEmpty: { [weak self] filter in
self?.filterBecameEmpty(filter)
}, emptyAction: { [weak self] filter in
self?.filterEmptyAction(filter)
})
self.itemNodes[.all] = itemNode
self.addSubnode(itemNode)
self._ready.set(itemNode.listNode.ready)
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.index(where: { $0.id == strongSelf.selectedId }) else {
return []
}
var directions: InteractiveTransitionGestureRecognizerDirections = [.left, .right]
if strongSelf.availableFilters.count > 1 {
if index == 0 {
directions.remove(.right)
}
if index == strongSelf.availableFilters.count - 1 {
directions.remove(.left)
}
} else {
directions = []
}
return directions
})
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer)
}
deinit {
self.pendingItemNode?.2.dispose()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .changed:
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.index(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)
}
if selectedIndex >= self.availableFilters.count - 1 {
transitionFraction = max(0.0, transitionFraction)
}
self.transitionFraction = transitionFraction
if let currentItemNode = self.currentItemNodeValue {
let isNavigationHidden = currentItemNode.listNode.isNavigationHidden
for (_, itemNode) in self.itemNodes {
if itemNode !== currentItemNode {
itemNode.listNode.adjustScrollOffsetForNavigation(isNavigationHidden: isNavigationHidden)
}
}
}
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate)
}
case .cancelled, .ended:
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.index(where: { $0.id == self.selectedId }) {
let translation = recognizer.translation(in: self.view)
let velocity = recognizer.velocity(in: self.view)
var directionIsToRight: Bool?
if abs(velocity.x) > 10.0 {
directionIsToRight = velocity.x < 0.0
} else {
if abs(translation.x) > layout.size.width / 2.0 {
directionIsToRight = translation.x > layout.size.width / 2.0
}
}
if let directionIsToRight = directionIsToRight {
var updatedIndex = selectedIndex
if directionIsToRight {
updatedIndex = min(updatedIndex + 1, self.availableFilters.count - 1)
} else {
updatedIndex = max(updatedIndex - 1, 0)
}
let switchToId = self.availableFilters[updatedIndex].id
if switchToId != self.selectedId, let itemNode = self.itemNodes[switchToId] {
self.selectedId = switchToId
self.applyItemNodeAsCurrent(id: switchToId, itemNode: itemNode)
}
}
self.transitionFraction = 0.0
let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
self.disableItemNodeOperationsWhileAnimating = true
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition)
transition.updateBounds(node: self, bounds: self.bounds, force: true, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.disableItemNodeOperationsWhileAnimating = false
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = strongSelf.validLayout {
strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
}
})
}
default:
break
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
for (_, itemNode) in self.itemNodes {
itemNode.updatePresentationData(presentationData)
}
}
func playArchiveAnimation() {
if let itemNode = self.itemNodes[self.selectedId] {
itemNode.listNode.forEachVisibleItemNode { node in
if let node = node as? ChatListItemNode {
node.playArchiveAnimation()
}
}
}
}
func scrollToTop() {
if let itemNode = self.itemNodes[self.selectedId] {
itemNode.listNode.scrollToPosition(.top)
}
}
func updateSelectedChatLocation(data: ChatLocation?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
for (_, itemNode) in self.itemNodes {
itemNode.listNode.updateSelectedChatLocation(data, progress: progress, transition: transition)
}
}
func updateState(_ f: (ChatListNodeState) -> ChatListNodeState) {
self.currentItemNode.updateState(f)
let updatedState = self.currentItemNode.currentState
for (id, itemNode) in self.itemNodes {
if id != self.selectedId {
itemNode.listNode.updateState { state in
var state = state
state.editing = updatedState.editing
state.selectedPeerIds = updatedState.selectedPeerIds
return state
}
}
}
}
func updateAvailableFilters(_ availableFilters: [ChatListContainerNodeFilter]) {
if self.availableFilters != availableFilters {
self.availableFilters = availableFilters
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout {
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
}
}
}
func switchToFilter(id: ChatListFilterTabEntryId) {
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout else {
return
}
if id != self.selectedId, let index = self.availableFilters.index(where: { $0.id == id }) {
if let itemNode = self.itemNodes[id] {
self.selectedId = id
if let currentItemNode = self.currentItemNodeValue {
itemNode.listNode.adjustScrollOffsetForNavigation(isNavigationHidden: currentItemNode.listNode.isNavigationHidden)
}
self.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition)
} 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)
}, emptyAction: { [weak self] filter in
self?.filterEmptyAction(filter)
})
let disposable = MetaDisposable()
self.pendingItemNode = (id, itemNode, disposable)
disposable.set((itemNode.listNode.ready
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in
guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else {
return
}
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = strongSelf.validLayout else {
return
}
strongSelf.pendingItemNode = nil
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
if let previousIndex = strongSelf.availableFilters.index(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.index(where: { $0.id == id }) {
let previousId = strongSelf.selectedId
let offsetDirection: CGFloat = index < previousIndex ? 1.0 : -1.0
let offset = offsetDirection * layout.size.width
var validNodeIds: [ChatListFilterTabEntryId] = []
for i in max(0, index - 1) ... min(strongSelf.availableFilters.count - 1, index + 1) {
validNodeIds.append(strongSelf.availableFilters[i].id)
}
var removeIds: [ChatListFilterTabEntryId] = []
for (id, _) in strongSelf.itemNodes {
if !validNodeIds.contains(id) {
removeIds.append(id)
}
}
for id in removeIds {
if let itemNode = strongSelf.itemNodes.removeValue(forKey: id) {
if id == previousId {
transition.updateFrame(node: itemNode, frame: itemNode.frame.offsetBy(dx: offset, dy: 0.0), completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
} else {
itemNode.removeFromSupernode()
}
}
}
strongSelf.itemNodes[id] = itemNode
strongSelf.addSubnode(itemNode)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)
itemNode.frame = itemFrame
transition.animatePositionAdditive(node: itemNode, offset: CGPoint(x: -offset, y: 0.0))
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, transition: .immediate)
strongSelf.selectedId = id
if let currentItemNode = strongSelf.currentItemNodeValue {
itemNode.listNode.adjustScrollOffsetForNavigation(isNavigationHidden: currentItemNode.listNode.isNavigationHidden)
}
strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, transition)
}
}))
}
}
}
func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
if let selectedIndex = self.availableFilters.index(where: { $0.id == self.selectedId }) {
var validNodeIds: [ChatListFilterTabEntryId] = []
for i in max(0, selectedIndex - 1) ... min(self.availableFilters.count - 1, selectedIndex + 1) {
let id = self.availableFilters[i].id
validNodeIds.append(id)
if self.itemNodes[id] == nil && !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
self?.filterEmptyAction(filter)
})
self.itemNodes[id] = itemNode
}
}
var removeIds: [ChatListFilterTabEntryId] = []
var animateSlidingIds: [ChatListFilterTabEntryId] = []
var slidingOffset: CGFloat?
for (id, itemNode) in self.itemNodes {
if !validNodeIds.contains(id) {
removeIds.append(id)
}
guard let index = self.availableFilters.index(where: { $0.id == id }) else {
continue
}
let indexDistance = CGFloat(index - selectedIndex) + self.transitionFraction
let wasAdded = itemNode.supernode == nil
var nodeTransition = transition
if wasAdded {
self.addSubnode(itemNode)
nodeTransition = .immediate
}
let itemFrame = CGRect(origin: CGPoint(x: indexDistance * layout.size.width, y: 0.0), size: layout.size)
if !wasAdded && slidingOffset == nil {
slidingOffset = itemNode.frame.minX - itemFrame.minX
}
nodeTransition.updateFrame(node: itemNode, frame: itemFrame, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
})
itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, transition: nodeTransition)
if wasAdded, case .animated = transition {
animateSlidingIds.append(id)
}
}
if let slidingOffset = slidingOffset {
for id in animateSlidingIds {
if let itemNode = self.itemNodes[id] {
transition.animatePositionAdditive(node: itemNode, offset: CGPoint(x: slidingOffset, y: 0.0), completion: {
})
}
}
}
if !self.disableItemNodeOperationsWhileAnimating {
for id in removeIds {
if let itemNode = self.itemNodes.removeValue(forKey: id) {
itemNode.removeFromSupernode()
}
}
}
}
return lhs.value < rhs.value
}
}
@ -47,9 +607,7 @@ final class ChatListControllerNode: ASDisplayNode {
private let groupId: PeerGroupId
private var presentationData: PresentationData
private var chatListEmptyNodeContainer: ChatListEmptyNodeContainer
private var chatListEmptyIndicator: ActivityIndicator?
let chatListNode: ChatListNode
let containerNode: ChatListContainerNode
var navigationBar: NavigationBar?
weak var controller: ChatListControllerImpl?
@ -78,8 +636,13 @@ final class ChatListControllerNode: ASDisplayNode {
self.groupId = groupId
self.presentationData = presentationData
self.chatListEmptyNodeContainer = ChatListEmptyNodeContainer(theme: presentationData.theme, strings: presentationData.strings)
self.chatListNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
var filterBecameEmpty: ((ChatListFilter?) -> Void)?
var filterEmptyAction: ((ChatListFilter?) -> Void)?
self.containerNode = ChatListContainerNode(context: context, groupId: groupId, previewing: previewing, presentationData: presentationData, filterBecameEmpty: { filter in
filterBecameEmpty?(filter)
}, filterEmptyAction: { filter in
filterEmptyAction?(filter)
})
self.controller = controller
@ -91,35 +654,24 @@ final class ChatListControllerNode: ASDisplayNode {
self.backgroundColor = presentationData.theme.chatList.backgroundColor
self.addSubnode(self.chatListNode)
self.addSubnode(self.chatListEmptyNodeContainer)
self.chatListNode.isEmptyUpdated = { [weak self] isEmptyState, isFilter, transitionDirection, transition in
self.addSubnode(self.containerNode)
self.addSubnode(self.debugListView)
filterBecameEmpty = { [weak self] _ in
guard let strongSelf = self else {
return
}
switch isEmptyState {
case .empty(false):
if case .group = strongSelf.groupId {
strongSelf.dismissSelf?()
} else {
strongSelf.chatListEmptyNodeContainer.update(state: isEmptyState, isFilter: isFilter, direction: transitionDirection, transition: transition)
}
case .notEmpty(false):
if case .group = strongSelf.groupId {
strongSelf.dismissSelf?()
} else {
strongSelf.chatListEmptyNodeContainer.update(state: isEmptyState, isFilter: isFilter, direction: transitionDirection, transition: transition)
}
default:
strongSelf.chatListEmptyNodeContainer.update(state: isEmptyState, isFilter: isFilter, direction: transitionDirection, transition: transition)
if case .group = strongSelf.groupId {
strongSelf.dismissSelf?()
}
}
self.chatListEmptyNodeContainer.action = { [weak self] in
self?.emptyListAction?()
filterEmptyAction = { [weak self] filter in
guard let strongSelf = self else {
return
}
strongSelf.emptyListAction?()
}
self.addSubnode(self.debugListView)
}
override func didLoad() {
@ -133,9 +685,8 @@ final class ChatListControllerNode: ASDisplayNode {
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)
self.containerNode.updatePresentationData(presentationData)
self.searchDisplayController?.updatePresentationData(presentationData)
self.chatListEmptyNodeContainer.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings)
if let toolbarNode = self.toolbarNode {
toolbarNode.updateTheme(TabBarControllerTheme(rootControllerTheme: self.presentationData.theme))
@ -194,23 +745,8 @@ final class ChatListControllerNode: ASDisplayNode {
})
}
self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve)
self.chatListNode.visualInsets = UIEdgeInsets(top: visualNavigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
let emptySize = CGSize(width: updateSizeAndInsets.size.width, height: updateSizeAndInsets.size.height - updateSizeAndInsets.insets.top - updateSizeAndInsets.insets.bottom)
transition.updateFrame(node: self.chatListEmptyNodeContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: updateSizeAndInsets.insets.top), size: emptySize))
self.chatListEmptyNodeContainer.updateLayout(size: emptySize, transition: transition)
if let chatListEmptyIndicator = self.chatListEmptyIndicator {
let indicatorSize = chatListEmptyIndicator.measure(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: chatListEmptyIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: updateSizeAndInsets.insets.top + floor((layout.size.height - updateSizeAndInsets.insets.top - updateSizeAndInsets.insets.bottom - indicatorSize.height) / 2.0)), size: indicatorSize))
}
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.containerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition)
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: transition)
@ -242,7 +778,7 @@ final class ChatListControllerNode: ASDisplayNode {
requestDeactivateSearch()
}
})
self.chatListNode.accessibilityElementsHidden = true
self.containerNode.accessibilityElementsHidden = true
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
@ -260,23 +796,19 @@ final class ChatListControllerNode: ASDisplayNode {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.deactivate(placeholder: placeholderNode, animated: animated)
self.searchDisplayController = nil
self.chatListNode.accessibilityElementsHidden = false
self.containerNode.accessibilityElementsHidden = false
}
}
func playArchiveAnimation() {
self.chatListNode.forEachVisibleItemNode { node in
if let node = node as? ChatListItemNode {
node.playArchiveAnimation()
}
}
self.containerNode.playArchiveAnimation()
}
func scrollToTop() {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.contentNode.scrollToTop()
} else {
self.chatListNode.scrollToPosition(.top)
self.containerNode.scrollToTop()
}
}
}

View File

@ -18,13 +18,15 @@ private final class ChatListFilterPresetControllerArguments {
let openAddPeer: () -> Void
let deleteAdditionalPeer: (PeerId) -> Void
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let focusOnName: () -> Void
init(context: AccountContext, updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void, openAddPeer: @escaping () -> Void, deleteAdditionalPeer: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) {
init(context: AccountContext, updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void, openAddPeer: @escaping () -> Void, deleteAdditionalPeer: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, focusOnName: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.openAddPeer = openAddPeer
self.deleteAdditionalPeer = deleteAdditionalPeer
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.focusOnName = focusOnName
}
}
@ -168,13 +170,15 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case let .nameHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .name(placeholder, value):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), sectionId: self.section, textUpdated: { value in
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), clearType: .always, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.name = value
return state
}
}, action: {})
}, action: {}, cleared: {
arguments.focusOnName()
})
case let .typesHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .filterPrivateChats(title, value):
@ -276,7 +280,7 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
}
func chatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter) -> ViewController {
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: true), options: []))
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection, options: []))
controller.navigationPresentation = .modal
let _ = (controller.result
|> take(1)
@ -285,7 +289,7 @@ func chatListFilterAddChatsController(context: AccountContext, filter: ChatListF
var settings = settings
for i in 0 ..< settings.filters.count {
if settings.filters[i].id == filter.id {
let previousIncludePeers = settings.filters[i].includePeers
let previousIncludePeers = settings.filters[i].data.includePeers
var chatPeerIds: [PeerId] = []
for peerId in peerIds {
@ -296,7 +300,7 @@ func chatListFilterAddChatsController(context: AccountContext, filter: ChatListF
break
}
}
settings.filters[i].includePeers = chatPeerIds + previousIncludePeers.filter { peerId in
settings.filters[i].data.includePeers = chatPeerIds + previousIncludePeers.filter { peerId in
return !chatPeerIds.contains(peerId)
}
}
@ -305,6 +309,8 @@ func chatListFilterAddChatsController(context: AccountContext, filter: ChatListF
})
|> deliverOnMainQueue).start(next: { settings in
controller?.dismiss()
let _ = replaceRemoteChatListFilters(account: context.account).start()
})
})
return controller
@ -317,7 +323,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} else {
initialName = "New Filter"
}
let initialState = ChatListFilterPresetControllerState(name: initialName, includeCategories: currentPreset?.categories ?? .all, excludeMuted: currentPreset?.excludeMuted ?? false, excludeRead: currentPreset?.excludeRead ?? false, additionallyIncludePeers: currentPreset?.includePeers ?? [])
let initialState = ChatListFilterPresetControllerState(name: initialName, includeCategories: currentPreset?.data.categories ?? .all, excludeMuted: currentPreset?.data.excludeMuted ?? false, excludeRead: currentPreset?.data.excludeRead ?? false, additionallyIncludePeers: currentPreset?.data.includePeers ?? [])
let stateValue = Atomic(value: initialState)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in
@ -331,6 +337,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var dismissImpl: (() -> Void)?
var focusOnNameImpl: (() -> Void)?
let arguments = ChatListFilterPresetControllerArguments(
context: context,
@ -338,7 +345,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
updateState(f)
},
openAddPeer: {
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: true), options: []))
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection, options: []))
addPeerDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] peerIds in
@ -377,6 +384,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
return state
}
},
focusOnName: {
focusOnNameImpl?()
}
)
@ -409,7 +419,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
})
let rightNavigationButton = ItemListNavigationButton(content: .text(currentPreset == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: {
let state = stateValue.with { $0 }
let preset = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, includePeers: state.additionallyIncludePeers)
let preset = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, includePeers: state.additionallyIncludePeers))
let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in
var preset = preset
if currentPreset == nil {
@ -435,6 +445,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|> deliverOnMainQueue).start(next: { settings in
updated(settings.filters)
dismissImpl?()
let _ = replaceRemoteChatListFilters(account: context.account).start()
})
})
@ -455,6 +467,16 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
dismissImpl = { [weak controller] in
let _ = controller?.dismiss()
}
focusOnNameImpl = { [weak controller] in
guard let controller = controller else {
return
}
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListSingleLineInputItemNode {
itemNode.focus()
}
}
}
return controller
}

View File

@ -53,7 +53,7 @@ private enum ChatListFilterPresetListEntryStableId: Hashable {
private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case listHeader(String)
case preset(index: Int, title: String?, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool)
case preset(index: Int, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool)
case addItem(text: String, isEditing: Bool)
case listFooter(String)
@ -99,8 +99,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
switch self {
case let .listHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .preset(index, title, preset, canBeReordered, canBeDeleted, isEditing):
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title ?? "", editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, sectionId: self.section, action: {
case let .preset(index, title, label, preset, canBeReordered, canBeDeleted, isEditing):
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title ?? "", label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, sectionId: self.section, action: {
arguments.openPreset(preset)
}, setItemWithRevealedOptions: { lhs, rhs in
arguments.setItemWithRevealedOptions(lhs, rhs)
@ -122,14 +122,14 @@ private struct ChatListFilterPresetListControllerState: Equatable {
var revealedPreset: Int32? = nil
}
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filtersState: ChatListFiltersState, settings: ChatListFilterSettings) -> [ChatListFilterPresetListEntry] {
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], settings: ChatListFilterSettings) -> [ChatListFilterPresetListEntry] {
var entries: [ChatListFilterPresetListEntry] = []
entries.append(.listHeader("FILTERS"))
for preset in filtersState.filters {
entries.append(.preset(index: entries.count, title: preset.title, preset: preset, canBeReordered: filtersState.filters.count > 1, canBeDeleted: true, isEditing: state.isEditing))
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))
}
if filtersState.filters.count < 10 {
if filters.count < 10 {
entries.append(.addItem(text: "Create New Filter", isEditing: state.isEditing))
}
entries.append(.listFooter("Tap \"Edit\" to change the order or delete filters."))
@ -137,7 +137,7 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
return entries
}
func chatListFilterPresetListController(context: AccountContext, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController {
public func chatListFilterPresetListController(context: AccountContext, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController {
let initialState = ChatListFilterPresetListControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@ -171,21 +171,51 @@ func chatListFilterPresetListController(context: AccountContext, updated: @escap
})
|> deliverOnMainQueue).start(next: { settings in
updated(settings.filters)
let _ = replaceRemoteChatListFilters(account: context.account).start()
})
})
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFilters, ApplicationSpecificPreferencesKeys.chatListFilterSettings])
let chatCountCache = Atomic<[ChatListFilterData: Int]>(value: [:])
let filtersWithCounts = 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 filters.map { filter -> (ChatListFilter, Int) in
let count: Int
if let cachedValue = chatCountCache.with({ dict -> Int? in
return dict[filter.data]
}) {
count = cachedValue
} else {
count = transaction.getChatCountMatchingPredicate(chatListFilterPredicate(filter: filter.data))
let _ = chatCountCache.modify { dict in
var dict = dict
dict[filter.data] = count
return dict
}
}
return (filter, count)
}
}
}
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get(),
filtersWithCounts,
preferences
)
|> map { presentationData, state, preferences -> (ItemListControllerState, (ItemListNodeState, Any)) in
let filtersState = preferences.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState ?? ChatListFiltersState.default
|> map { presentationData, state, filtersWithCounts, preferences -> (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: {
let _ = replaceRemoteChatListFilters(account: context.account).start()
dismissImpl?()
})
let rightNavigationButton: ItemListNavigationButton
@ -208,7 +238,7 @@ func chatListFilterPresetListController(context: AccountContext, updated: @escap
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Filters"), 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, filtersState: filtersState, settings: filterSettings), style: .blocks, animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCounts, settings: filterSettings), style: .blocks, animateChanges: true)
return (controllerState, (listState, arguments))
}
@ -217,6 +247,9 @@ func chatListFilterPresetListController(context: AccountContext, updated: @escap
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
controller.willDisappear = { _ in
let _ = replaceRemoteChatListFilters(account: context.account).start()
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}

View File

@ -20,6 +20,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let preset: ChatListFilter
let title: String
let label: String
let editing: ChatListFilterPresetListItemEditing
let canBeReordered: Bool
let canBeDeleted: Bool
@ -32,6 +33,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
presentationData: ItemListPresentationData,
preset: ChatListFilter,
title: String,
label: String,
editing: ChatListFilterPresetListItemEditing,
canBeReordered: Bool,
canBeDeleted: Bool,
@ -43,6 +45,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
self.presentationData = presentationData
self.preset = preset
self.title = title
self.label = label
self.editing = editing
self.canBeReordered = canBeReordered
self.canBeDeleted = canBeDeleted
@ -108,6 +111,8 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
private let maskNode: ASImageNode
private let titleNode: TextNode
private let labelNode: TextNode
private let arrowNode: ASImageNode
private let activateArea: AccessibilityAreaNode
@ -141,6 +146,14 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
self.highlightedBackgroundNode = ASDisplayNode()
@ -149,6 +162,8 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in
@ -159,6 +174,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
func asyncLayout() -> (_ item: ChatListFilterPresetListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
@ -166,9 +182,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
var updateArrowImage: UIImage?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
}
let peerRevealOptions: [ItemListRevealOption]
@ -187,21 +205,27 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
var editingOffset: CGFloat = 0.0
var reorderInset: CGFloat = 0.0
if item.editing.editing && item.canBeReordered {
if item.editing.editing {
let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = reorderSizeAndApply
reorderInset = reorderSizeAndApply.0
if item.canBeReordered {
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = reorderSizeAndApply
reorderInset = reorderSizeAndApply.0
}
}
let leftInset: CGFloat = 16.0 + params.leftInset
let rightInset: CGFloat = params.rightInset + max(reorderInset, 55.0)
let rightArrowInset: CGFloat = 34.0 + params.rightInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let labelConstrain: CGFloat = params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: titleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let insets = itemListNeighborsGroupedInsets(neighbors)
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + 11.0 * 2.0)
let separatorHeight = UIScreenPixel
@ -280,6 +304,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}
let _ = titleApply()
let _ = labelApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
@ -326,6 +351,20 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size))
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width, y: 11.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.arrowNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
if let updateArrowImage = updateArrowImage {
strongSelf.arrowNode.image = updateArrowImage
}
if let arrowImage = strongSelf.arrowNode.image {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
}
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))

View File

@ -257,13 +257,12 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
}
}
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
let focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter
var previousSelectedAbsFrame: CGRect?
private var previousSelectedAbsFrame: CGRect?
private var previousSelectedFrame: CGRect?
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, transitionFraction: CGFloat, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter
let previousScrollBounds = self.scrollNode.bounds
if let currentSelectedFilter = self.currentParams?.selectedFilter, let itemNode = self.itemNodes[currentSelectedFilter] {
previousSelectedAbsFrame = itemNode.frame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0)
}
if self.currentParams?.presentationData.theme !== presentationData.theme {
self.selectedLineNode.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in
@ -362,7 +361,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
}
}
let minSpacing: CGFloat = 30.0
let minSpacing: CGFloat = 26.0
let sideInset: CGFloat = 16.0
var leftOffset: CGFloat = sideInset
@ -391,7 +390,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
self.scrollNode.view.contentSize = CGSize(width: leftOffset - minSpacing + sideInset - 5.0, height: size.height)
let transitionFraction: CGFloat = 0.0
var previousFrame: CGRect?
var nextFrame: CGRect?
var selectedFrame: CGRect?
if let selectedFilter = selectedFilter, let currentIndex = filters.index(where: { $0.id == selectedFilter }) {
func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
@ -422,28 +422,41 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
} else {
transition.updateFrame(node: self.selectedLineNode, frame: lineFrame)
}
if !transitionFraction.isZero {
if previousScrollBounds.minX.isZero {
focusOnSelectedFilter = true
} else if previousScrollBounds.maxX == previousScrollBounds.width {
focusOnSelectedFilter = true
} else if let previousSelectedFrame = self.previousSelectedFrame, abs(previousSelectedFrame.offsetBy(dx: -previousScrollBounds.minX, dy: 0.0).midX - previousScrollBounds.width / 2.0) < 1.0 {
focusOnSelectedFilter = true
}
}
if focusOnSelectedFilter {
if selectedFilter == filters.first?.id {
if transitionFraction.isZero && selectedFilter == filters.first?.id {
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size))
} else if selectedFilter == filters.last?.id {
} else if transitionFraction.isZero && selectedFilter == filters.last?.id {
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size))
} else {
let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0)))
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size))
}
} else if !wasAdded, let previousSelectedAbsFrame = previousSelectedAbsFrame {
} else if !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 = selectedFrame.midX - previousSelectedAbsFrame.midX
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 {
self.selectedLineNode.isHidden = true
self.previousSelectedAbsFrame = nil
self.previousSelectedFrame = nil
}
}
}

View File

@ -17,7 +17,7 @@ import SearchUI
public enum ChatListNodeMode {
case chatList
case peers(filter: ChatListNodePeersFilter)
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool)
}
struct ChatListNodeListViewTransition {
@ -153,7 +153,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
switch mode {
case .chatList:
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, isInFilter: isInFilter, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .peers(filter):
case let .peers(filter, _):
let itemPeer = peer.chatMainPeer
var chatPeer: Peer?
if let peer = peer.peers[peer.peerId] {
@ -218,7 +218,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
}
}
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in
if let chatPeer = chatPeer {
nodeInteraction.peerSelected(chatPeer)
}
@ -245,7 +245,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
switch mode {
case .chatList:
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, isInFilter: isInFilter, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .peers(filter):
case let .peers(filter, _):
let itemPeer = peer.chatMainPeer
var chatPeer: Peer?
if let peer = peer.peers[peer.peerId] {
@ -266,7 +266,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
enabled = false
}
}
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in
if let chatPeer = chatPeer {
nodeInteraction.peerSelected(chatPeer)
}
@ -352,7 +352,7 @@ public final class ChatListNode: ListView {
return _contentsReady.get()
}
public var peerSelected: ((PeerId, Bool, Bool) -> Void)?
public var peerSelected: ((Peer, Bool, Bool) -> Void)?
public var disabledPeerSelected: ((Peer) -> Void)?
public var groupSelected: ((PeerGroupId) -> Void)?
public var addContact: ((String) -> Void)?
@ -373,7 +373,7 @@ public final class ChatListNode: ListView {
private var dequeuedInitialTransitionOnLayout = false
private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)?
private(set) var currentState: ChatListNodeState
public private(set) var currentState: ChatListNodeState
private let statePromise: ValuePromise<ChatListNodeState>
public var state: Signal<ChatListNodeState, NoError> {
return self.statePromise.get()
@ -453,7 +453,12 @@ public final class ChatListNode: ListView {
self.controlsHistoryPreload = controlsHistoryPreload
self.mode = mode
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: false, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false)
var isSelecting = false
if case .peers(_, true) = mode {
isSelecting = true
}
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false)
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
self.theme = theme
@ -471,7 +476,7 @@ public final class ChatListNode: ListView {
}
}, peerSelected: { [weak self] peer in
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
peerSelected(peer.id, true, false)
peerSelected(peer, true, false)
}
}, disabledPeerSelected: { [weak self] peer in
if let strongSelf = self, let disabledPeerSelected = strongSelf.disabledPeerSelected {
@ -491,7 +496,7 @@ public final class ChatListNode: ListView {
}
}, messageSelected: { [weak self] peer, message, isAd in
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
peerSelected(peer.id, true, isAd)
peerSelected(peer, true, isAd)
}
}, groupSelected: { [weak self] groupId in
if let strongSelf = self, let groupSelected = strongSelf.groupSelected {
@ -586,7 +591,7 @@ public final class ChatListNode: ListView {
let currentRemovingPeerId = self.currentRemovingPeerId
let savedMessagesPeer: Signal<Peer?, NoError>
if case let .peers(filter) = mode, filter.contains(.onlyWriteable) {
if case let .peers(filter, _) = mode, filter.contains(.onlyWriteable) {
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map(Optional.init)
} else {
@ -639,7 +644,7 @@ public final class ChatListNode: ListView {
switch mode {
case .chatList:
return true
case let .peers(filter):
case let .peers(filter, _):
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false }
@ -1422,6 +1427,35 @@ public final class ChatListNode: ListView {
}
}
var isNavigationHidden: Bool {
switch self.visibleContentOffset() {
case let .known(value) where abs(value) < navigationBarSearchContentHeight:
return false
default:
return true
}
}
func adjustScrollOffsetForNavigation(isNavigationHidden: Bool) {
if self.isNavigationHidden == isNavigationHidden {
return
}
var scrollToItem: ListViewScrollToItem?
switch self.visibleContentOffset() {
case let .known(value) where abs(value) < navigationBarSearchContentHeight:
if isNavigationHidden {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
}
default:
if !isNavigationHidden {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
}
}
if let scrollToItem = scrollToItem {
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) {
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
@ -1524,9 +1558,9 @@ public final class ChatListNode: ListView {
}
let entryCount = chatListView.filteredEntries.count
var current: (ChatListIndex, PeerId, Int)? = nil
var previous: (ChatListIndex, PeerId)? = nil
var next: (ChatListIndex, PeerId)? = nil
var current: (ChatListIndex, Peer, Int)? = nil
var previous: (ChatListIndex, Peer)? = nil
var next: (ChatListIndex, Peer)? = nil
outer: for i in range.firstIndex ..< range.lastIndex {
if i < 0 || i >= entryCount {
@ -1536,7 +1570,7 @@ public final class ChatListNode: ListView {
switch chatListView.filteredEntries[entryCount - i - 1] {
case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _):
if interaction.highlightedChatLocation?.location == ChatLocation.peer(peer.peerId) {
current = (index, peer.peerId, entryCount - i - 1)
current = (index, peer.peer!, entryCount - i - 1)
break outer
}
default:
@ -1556,22 +1590,35 @@ public final class ChatListNode: ListView {
} else {
position = .later(than: nil)
}
let _ = (relativeUnreadChatListIndex(position: position) |> deliverOnMainQueue).start(next: { [weak self] index in
guard let strongSelf = self, let index = index else {
let postbox = self.context.account.postbox
let _ = (relativeUnreadChatListIndex(position: position)
|> mapToSignal { index -> Signal<(ChatListIndex, Peer)?, NoError> in
if let index = index {
return postbox.transaction { transaction -> (ChatListIndex, Peer)? in
return transaction.getPeer(index.messageIndex.id.peerId).flatMap { peer -> (ChatListIndex, Peer)? in
(index, peer)
}
}
} else {
return .single(nil)
}
}
|> deliverOnMainQueue).start(next: { [weak self] indexAndPeer in
guard let strongSelf = self, let (index, peer) = indexAndPeer else {
return
}
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: strongSelf.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound, scrollPosition: .center(.top), animated: true, filter: strongSelf.chatListFilter)
strongSelf.setChatListLocation(location)
strongSelf.peerSelected?(index.messageIndex.id.peerId, false, false)
strongSelf.peerSelected?(peer, false, false)
})
case .previous(unread: false), .next(unread: false):
var target: (ChatListIndex, PeerId)? = nil
var target: (ChatListIndex, Peer)? = nil
if let current = current, entryCount > 1 {
if current.2 > 0, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 - 1] {
next = (index, peer.peerId)
next = (index, peer.peer!)
}
if current.2 <= entryCount - 2, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 + 1] {
previous = (index, peer.peerId)
previous = (index, peer.peer!)
}
if case .previous = option {
target = previous
@ -1580,7 +1627,7 @@ public final class ChatListNode: ListView {
}
} else if entryCount > 0 {
if case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[entryCount - 1] {
target = (index, peer.peerId)
target = (index, peer.peer!)
}
}
if let target = target {
@ -1589,7 +1636,15 @@ public final class ChatListNode: ListView {
self.peerSelected?(target.1, false, false)
}
case let .peerId(peerId):
self.peerSelected?(peerId, false, false)
let _ = (self.context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self, let peer = peer else {
return
}
strongSelf.peerSelected?(peer, false, false)
})
case let .index(index):
guard index < 10 else {
return
@ -1607,7 +1662,7 @@ public final class ChatListNode: ListView {
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _) = entries[10 - index - 1] {
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter)
self.setChatListLocation(location)
self.peerSelected?(renderedPeer.peerId, false, false)
self.peerSelected?(renderedPeer.peer!, false, false)
}
})
})

View File

@ -29,7 +29,7 @@ struct ChatListNodeViewUpdate {
let scrollPosition: ChatListNodeViewScrollPosition?
}
func chatListFilterPredicate(filter: ChatListFilter) -> ChatListFilterPredicate {
func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredicate {
let includePeers = Set(filter.includePeers)
return ChatListFilterPredicate(includePeerIds: includePeers, include: { peer, notificationSettings, isUnread in
if filter.excludeRead {
@ -97,7 +97,7 @@ func chatListFilterPredicate(filter: ChatListFilter) -> ChatListFilterPredicate
}
func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> {
let filterPredicate: ChatListFilterPredicate? = location.filter.flatMap(chatListFilterPredicate)
let filterPredicate: ChatListFilterPredicate? = (location.filter?.data).flatMap(chatListFilterPredicate)
switch location {
case let .initial(count, _):

View File

@ -320,7 +320,7 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
unreadCountItems.append(.total(nil))
var additionalPeerIds = Set<PeerId>()
for filter in filters {
additionalPeerIds.formUnion(filter.includePeers)
additionalPeerIds.formUnion(filter.data.includePeers)
}
if !additionalPeerIds.isEmpty {
for peerId in additionalPeerIds {
@ -390,22 +390,22 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
var shouldUpdateLayout = false
for filter in filters {
var tags: [PeerSummaryCounterTags] = []
if filter.categories.contains(.privateChats) {
if filter.data.categories.contains(.privateChats) {
tags.append(.privateChat)
}
if filter.categories.contains(.secretChats) {
if filter.data.categories.contains(.secretChats) {
tags.append(.secretChat)
}
if filter.categories.contains(.privateGroups) {
if filter.data.categories.contains(.privateGroups) {
tags.append(.privateGroup)
}
if filter.categories.contains(.bots) {
if filter.data.categories.contains(.bots) {
tags.append(.bot)
}
if filter.categories.contains(.publicGroups) {
if filter.data.categories.contains(.publicGroups) {
tags.append(.publicGroup)
}
if filter.categories.contains(.channels) {
if filter.data.categories.contains(.channels) {
tags.append(.channel)
}
@ -417,7 +417,7 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
}
}
}
for peerId in filter.includePeers {
for peerId in filter.data.includePeers {
if let (tag, peerCount) = peerTagAndCount[peerId] {
if !tags.contains(tag) {
if peerCount != 0 {
@ -513,7 +513,7 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
unreadCountItems.append(.total(nil))
var additionalPeerIds = Set<PeerId>()
for preset in presetList {
additionalPeerIds.formUnion(preset.includePeers)
additionalPeerIds.formUnion(preset.data.includePeers)
}
if !additionalPeerIds.isEmpty {
for peerId in additionalPeerIds {
@ -567,22 +567,22 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
let badgeString: String
if let preset = contentNode.preset {
var tags: [PeerSummaryCounterTags] = []
if preset.categories.contains(.privateChats) {
if preset.data.categories.contains(.privateChats) {
tags.append(.privateChat)
}
if preset.categories.contains(.secretChats) {
if preset.data.categories.contains(.secretChats) {
tags.append(.secretChat)
}
if preset.categories.contains(.privateGroups) {
if preset.data.categories.contains(.privateGroups) {
tags.append(.privateGroup)
}
if preset.categories.contains(.bots) {
if preset.data.categories.contains(.bots) {
tags.append(.bot)
}
if preset.categories.contains(.publicGroups) {
if preset.data.categories.contains(.publicGroups) {
tags.append(.publicGroup)
}
if preset.categories.contains(.channels) {
if preset.data.categories.contains(.channels) {
tags.append(.channel)
}
@ -594,7 +594,7 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
}
}
}
for peerId in preset.includePeers {
for peerId in preset.data.includePeers {
if let (tag, peerCount) = peerTagAndCount[peerId] {
if !tags.contains(tag) {
count += peerCount

View File

@ -747,7 +747,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false)
}
self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in
self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in
completedActionsNode = true
intermediateCompletion()
})

View File

@ -38,18 +38,23 @@ public struct InteractiveTransitionGestureRecognizerDirections: OptionSet {
self.rawValue = rawValue
}
public static let left = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 0)
public static let right = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 1)
public static let leftEdge = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 2)
public static let rightEdge = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 3)
public static let leftCenter = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 0)
public static let rightCenter = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 1)
public static let left: InteractiveTransitionGestureRecognizerDirections = [.leftEdge, .leftCenter]
public static let right: InteractiveTransitionGestureRecognizerDirections = [.rightEdge, .rightCenter]
}
public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
private let allowedDirections: () -> InteractiveTransitionGestureRecognizerDirections
private let allowedDirections: (CGPoint) -> InteractiveTransitionGestureRecognizerDirections
private var validatedGesture = false
private var firstLocation: CGPoint = CGPoint()
private var currentAllowedDirections: InteractiveTransitionGestureRecognizerDirections = []
public init(target: Any?, action: Selector?, allowedDirections: @escaping () -> InteractiveTransitionGestureRecognizerDirections) {
public init(target: Any?, action: Selector?, allowedDirections: @escaping (CGPoint) -> InteractiveTransitionGestureRecognizerDirections) {
self.allowedDirections = allowedDirections
super.init(target: target, action: action)
@ -65,37 +70,50 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
self.currentAllowedDirections = self.allowedDirections()
if self.currentAllowedDirections.isEmpty {
let touch = touches.first!
let point = touch.location(in: self.view)
var allowedDirections = self.allowedDirections(point)
if allowedDirections.isEmpty {
self.state = .failed
return
}
super.touchesBegan(touches, with: event)
let touch = touches.first!
self.firstLocation = touch.location(in: self.view)
self.firstLocation = point
if let target = self.view?.hitTest(self.firstLocation, with: event) {
if hasHorizontalGestures(target, point: self.view?.convert(self.firstLocation, to: target)) {
self.state = .cancelled
allowedDirections.remove(.leftCenter)
allowedDirections.remove(.rightCenter)
}
}
if allowedDirections.isEmpty {
self.state = .failed
} else {
self.currentAllowedDirections = allowedDirections
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
let location = touches.first!.location(in: self.view)
let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y)
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
let absTranslationX: CGFloat = abs(translation.x)
let absTranslationY: CGFloat = abs(translation.y)
let size = self.view?.bounds.size ?? CGSize()
if !self.validatedGesture {
if self.currentAllowedDirections.contains(.right) && self.firstLocation.x < 16.0 {
if self.currentAllowedDirections.contains(.rightEdge) && self.firstLocation.x < 16.0 {
self.validatedGesture = true
} else if !self.currentAllowedDirections.contains(.left) && translation.x < 0.0 {
} else if self.currentAllowedDirections.contains(.leftEdge) && self.firstLocation.x > size.width - 16.0 {
self.validatedGesture = true
} else if !self.currentAllowedDirections.contains(.leftCenter) && translation.x < 0.0 {
self.state = .failed
} else if !self.currentAllowedDirections.contains(.right) && translation.x > 0.0 {
} else if !self.currentAllowedDirections.contains(.rightCenter) && translation.x > 0.0 {
self.state = .failed
} else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 {
self.state = .failed
@ -104,7 +122,7 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
}
}
if validatedGesture {
if self.validatedGesture {
super.touchesMoved(touches, with: event)
}
}

View File

@ -111,7 +111,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
override func didLoad() {
super.didLoad()
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] in
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, strongSelf.controllers.count > 1 else {
return []
}

View File

@ -90,7 +90,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
self.scrollNode.view.clipsToBounds = false
self.scrollNode.view.delegate = self
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] in
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, !strongSelf.isDismissed else {
return []
}

View File

@ -25,10 +25,10 @@ final class TabBarControllerNode: ASDisplayNode {
}
}
init(theme: TabBarControllerTheme, navigationBar: NavigationBar?, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void) {
init(theme: TabBarControllerTheme, navigationBar: NavigationBar?, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void) {
self.theme = theme
self.navigationBar = navigationBar
self.tabBarNode = TabBarNode(theme: theme, itemSelected: itemSelected, contextAction: contextAction)
self.tabBarNode = TabBarNode(theme: theme, itemSelected: itemSelected, contextAction: contextAction, swipeAction: swipeAction)
self.toolbarActionSelected = toolbarActionSelected
super.init()

View File

@ -240,6 +240,13 @@ open class TabBarController: ViewController {
if index >= 0 && index < strongSelf.controllers.count {
strongSelf.controllers[index].tabBarItemContextAction(sourceNode: node, gesture: gesture)
}
}, swipeAction: { [weak self] index, direction in
guard let strongSelf = self else {
return
}
if index >= 0 && index < strongSelf.controllers.count {
strongSelf.controllers[index].tabBarItemSwipeAction(direction: direction)
}
}, toolbarActionSelected: { [weak self] action in
self?.currentController?.toolbarActionSelected(action: action)
})

View File

@ -88,6 +88,11 @@ private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor:
private let badgeFont = Font.regular(13.0)
public enum TabBarItemSwipeDirection {
case left
case right
}
private final class TabBarItemNode: ASDisplayNode {
let extractedContainerNode: ContextExtractedContentContainingNode
let containerNode: ContextControllerSourceNode
@ -97,6 +102,8 @@ private final class TabBarItemNode: ASDisplayNode {
let contextTextImageNode: ASImageNode
var contentWidth: CGFloat?
var swiped: ((TabBarItemSwipeDirection) -> Void)?
override init() {
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
@ -146,6 +153,26 @@ private final class TabBarItemNode: ASDisplayNode {
transition.updateAlpha(node: strongSelf.contextImageNode, alpha: isExtracted ? 1.0 : 0.0)
transition.updateAlpha(node: strongSelf.contextTextImageNode, alpha: isExtracted ? 1.0 : 0.0)
}
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(self.swipeGesture(_:)))
leftSwipe.direction = .left
self.containerNode.view.addGestureRecognizer(leftSwipe)
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(self.swipeGesture(_:)))
rightSwipe.direction = .right
self.containerNode.view.addGestureRecognizer(rightSwipe)
}
@objc private func swipeGesture(_ gesture: UISwipeGestureRecognizer) {
if case .ended = gesture.state {
self.containerNode.cancelGesture()
switch gesture.direction {
case .left:
self.swiped?(.left)
default:
self.swiped?(.right)
}
}
}
}
@ -173,7 +200,7 @@ private final class TabBarNodeContainer {
var selectedImageValue: UIImage?
var appliedSelectedImageValue: UIImage?
init(item: TabBarNodeItem, imageNode: TabBarItemNode, updateBadge: @escaping (String) -> Void, updateTitle: @escaping (String, Bool) -> Void, updateImage: @escaping (UIImage?) -> Void, updateSelectedImage: @escaping (UIImage?) -> Void, contextAction: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) {
init(item: TabBarNodeItem, imageNode: TabBarItemNode, updateBadge: @escaping (String) -> Void, updateTitle: @escaping (String, Bool) -> Void, updateImage: @escaping (UIImage?) -> Void, updateSelectedImage: @escaping (UIImage?) -> Void, contextAction: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (TabBarItemSwipeDirection) -> Void) {
self.item = item.item
self.imageNode = imageNode
@ -225,6 +252,9 @@ private final class TabBarNodeContainer {
}
contextAction(strongSelf.imageNode.extractedContainerNode, gesture)
}
imageNode.swiped = { [weak self] direction in
swipeAction(direction)
}
imageNode.containerNode.isGestureEnabled = item.hasContext
}
@ -269,6 +299,7 @@ class TabBarNode: ASDisplayNode {
private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void
private let contextAction: (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void
private let swipeAction: (Int, TabBarItemSwipeDirection) -> Void
private var theme: TabBarControllerTheme
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)?
@ -282,9 +313,10 @@ class TabBarNode: ASDisplayNode {
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
init(theme: TabBarControllerTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void) {
init(theme: TabBarControllerTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void) {
self.itemSelected = itemSelected
self.contextAction = contextAction
self.swipeAction = swipeAction
self.theme = theme
self.separatorNode = ASDisplayNode()
@ -381,6 +413,8 @@ class TabBarNode: ASDisplayNode {
}, contextAction: { [weak self] node, gesture in
self?.tapRecognizer?.cancel()
self?.contextAction(i, node, gesture)
}, swipeAction: { [weak self] direction in
self?.swipeAction(i, direction)
})
if let selectedIndex = self.selectedIndex, selectedIndex == i {
let (textImage, contentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)

View File

@ -632,6 +632,9 @@ public enum ViewControllerNavigationPresentation {
open func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
}
open func tabBarItemSwipeAction(direction: TabBarItemSwipeDirection) {
}
}
func traceIsOpaque(layer: CALayer, rect: CGRect) -> Bool {

View File

@ -91,6 +91,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, UIGestureRecognizerD
let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:)))
self.recognizer = recognizer
recognizer.delegate = self
recognizer.allowAnyDirection = self.allowAnyDirection
self.view.addGestureRecognizer(recognizer)

View File

@ -45,9 +45,10 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let shouldUpdateText: (String) -> Bool
let processPaste: ((String) -> String)?
let updatedFocus: ((Bool) -> Void)?
let cleared: (() -> Void)?
public let tag: ItemListItemTag?
public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, enabled: Bool = true, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void) {
public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, enabled: Bool = true, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
self.presentationData = presentationData
self.title = title
self.text = text
@ -64,6 +65,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
self.processPaste = processPaste
self.updatedFocus = updatedFocus
self.action = action
self.cleared = cleared
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -420,6 +422,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
@objc private func clearButtonPressed() {
self.textNode.textField.text = ""
self.textUpdated("")
self.item?.cleared?()
}
private func textUpdated(_ text: String) {

View File

@ -641,6 +641,25 @@ final class ChatListTable: Table {
return entries
}
func countWithPredicate(groupId: PeerGroupId, predicate: (PeerId) -> Bool) -> Int {
var result = 0
self.valueBox.filteredRange(self.table, start: self.lowerBound(groupId: groupId), end: self.upperBound(groupId: groupId), keys: { key in
let (_, _, messageIndex, type) = extractKey(key)
if type == ChatListEntryType.message.rawValue {
if predicate(messageIndex.id.peerId) {
result += 1
return .accept
} else {
return .skip
}
} else {
return .skip
}
}, limit: 10000)
return result
}
func getStandalone(peerId: PeerId, messageHistoryTable: MessageHistoryTable) -> ChatListIntermediateEntry? {
let index = self.indexTable.get(peerId: peerId)
switch index.inclusion {

View File

@ -320,6 +320,7 @@ final class MutableChatListView {
fileprivate var additionalItemIds: Set<PeerId>
fileprivate var additionalItemEntries: [MutableChatListEntry]
fileprivate var additionalMixedItemIds: Set<PeerId>
fileprivate var additionalMixedPinnedItemIds: Set<PeerId>
fileprivate var additionalMixedItemEntries: [MutableChatListEntry]
fileprivate var earlier: MutableChatListEntry?
fileprivate var later: MutableChatListEntry?
@ -340,10 +341,17 @@ final class MutableChatListView {
self.additionalItemEntries = []
self.additionalMixedItemEntries = []
self.additionalMixedItemIds = Set()
self.additionalMixedPinnedItemIds = Set()
if let filterPredicate = self.filterPredicate {
self.additionalMixedItemIds.formUnion(filterPredicate.includePeerIds)
for (itemId, _) in postbox.chatListTable.getPinnedItemIds(groupId: self.groupId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
switch itemId {
case let .peer(peerId):
self.additionalMixedPinnedItemIds.insert(peerId)
}
}
}
for peerId in self.additionalMixedItemIds {
for peerId in self.additionalMixedItemIds.union(self.additionalMixedPinnedItemIds) {
if let entry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
self.additionalMixedItemEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
}
@ -554,7 +562,7 @@ final class MutableChatListView {
if !updatedPeerNotificationSettings.isEmpty {
if let filterPredicate = self.filterPredicate {
for (peerId, settingsChange) in updatedPeerNotificationSettings {
if let peer = postbox.peerTable.get(peerId) {
if let peer = postbox.peerTable.get(peerId), !self.additionalMixedItemIds.contains(peerId), !self.additionalMixedPinnedItemIds.contains(peerId) {
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let wasIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.0, isUnread: isUnread)
let isIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.1, isUnread: isUnread)
@ -611,6 +619,22 @@ final class MutableChatListView {
continue
}
}
for i in 0 ..< self.additionalMixedItemEntries.count {
switch self.additionalMixedItemEntries[i] {
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
var notificationSettingsPeerId = peer.peerId
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
notificationSettingsPeerId = associatedPeerId
}
if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] {
self.additionalMixedItemEntries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed)
hasChanges = true
}
default:
continue
}
}
}
if !transaction.updatedFailedMessagePeerIds.isEmpty {
@ -723,7 +747,7 @@ final class MutableChatListView {
hasChanges = true
}
var updateAdditionalMixedItems = false
for peerId in self.additionalMixedItemIds {
for peerId in self.additionalMixedItemIds.union(self.additionalMixedPinnedItemIds) {
if transaction.currentOperationsByPeerId[peerId] != nil {
updateAdditionalMixedItems = true
}
@ -736,7 +760,7 @@ final class MutableChatListView {
}
if updateAdditionalMixedItems {
self.additionalMixedItemEntries.removeAll()
for peerId in self.additionalMixedItemIds {
for peerId in self.additionalMixedItemIds.union(self.additionalMixedPinnedItemIds) {
if let entry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
self.additionalMixedItemEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
}
@ -755,6 +779,12 @@ final class MutableChatListView {
if !filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread: isUnread) {
return false
}
if self.additionalMixedItemIds.contains(peer.id) {
return false
}
if self.additionalMixedPinnedItemIds.contains(peer.id) {
return false
}
} else {
return false
}
@ -1076,12 +1106,17 @@ public final class ChatListView {
existingIds.insert(messageEntry.0.messageIndex.id.peerId)
}
}
for entry in mutableView.additionalMixedItemEntries {
loop: for entry in mutableView.additionalMixedItemEntries {
if case let .MessageEntry(messageEntry) = entry {
if !existingIds.contains(messageEntry.0.messageIndex.id.peerId) {
switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
entries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed))
if let filterPredicate = mutableView.filterPredicate, let peerValue = peer.peer {
if filterPredicate.includes(peer: peerValue, notificationSettings: notificationSettings, isUnread: combinedReadState?.isUnread ?? false) {
existingIds.insert(messageEntry.0.messageIndex.id.peerId)
entries.append(.MessageEntry(ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex), message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed))
}
}
case let .HoleEntry(hole):
entries.append(.HoleEntry(hole))
case .IntermediateMessageEntry:
@ -1104,12 +1139,8 @@ public final class ChatListView {
additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed))
case .HoleEntry:
assertionFailure()
/*case .GroupReferenceEntry:
assertionFailure()*/
case .IntermediateMessageEntry:
assertionFailure()
/*case .IntermediateGroupReferenceEntry:
assertionFailure()*/
}
}

View File

@ -763,6 +763,39 @@ public final class Transaction {
}
}
public func getChatCountMatchingPredicate(_ predicate: ChatListFilterPredicate) -> Int {
assert(!self.disposed)
guard let postbox = self.postbox else {
return 0
}
var includedPeerIds: [PeerId: Bool] = [:]
for peerId in predicate.includePeerIds {
includedPeerIds[peerId] = false
}
var count = postbox.chatListTable.countWithPredicate(groupId: .root, predicate: { peerId in
if let peer = postbox.peerTable.get(peerId) {
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let notificationsPeerId = peer.notificationSettingsPeerId ?? peerId
if predicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread) {
includedPeerIds[peer.id] = true
return true
} else {
return false
}
} else {
return false
}
})
for (peerId, included) in includedPeerIds {
if !included {
if postbox.chatListTable.getPeerChatListIndex(peerId: peerId) != nil {
count += 1
}
}
}
return count
}
public func legacyGetAccessChallengeData() -> PostboxAccessChallengeData {
assert(!self.disposed)
if let postbox = self.postbox {
@ -1657,9 +1690,13 @@ public final class Postbox {
return { entry in
switch entry {
case let .message(index, _, _):
if index.pinningIndex != nil {
return false
}
if let peer = self.peerTable.get(index.messageIndex.id.peerId) {
let isUnread = self.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
if predicate.includes(peer: peer, notificationSettings: self.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread: isUnread) {
let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
if predicate.includes(peer: peer, notificationSettings: self.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread) {
return true
} else {
return false

View File

@ -1501,7 +1501,10 @@ public final class SqliteValueBox: ValueBox {
public func filteredRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> ValueBoxFilterResult, limit: Int) {
var currentStart = start
var acceptedCount = 0
while acceptedCount < limit {
while true {
if limit > 0 && acceptedCount >= limit {
break
}
var hadStop = false
var lastKey: ValueBoxKey?
self.range(table, start: currentStart, end: end, values: { key, value in
@ -1530,6 +1533,41 @@ public final class SqliteValueBox: ValueBox {
}
}
public func filteredRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> ValueBoxFilterResult, limit: Int) {
var currentStart = start
var acceptedCount = 0
while true {
if limit > 0 && acceptedCount >= limit {
break
}
var hadStop = false
var lastKey: ValueBoxKey?
self.range(table, start: currentStart, end: end, keys: { key in
lastKey = key
let result = keys(key)
switch result {
case .accept:
acceptedCount += 1
return true
case .skip:
return true
case .stop:
hadStop = true
return false
}
return true
}, limit: limit)
if let lastKey = lastKey {
currentStart = lastKey
} else {
break
}
if hadStop {
break
}
}
}
public func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> Bool, limit: Int) {
precondition(self.queue.isCurrent())
if let _ = self.tables[table.id] {

View File

@ -73,6 +73,7 @@ public protocol ValueBox {
func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> Bool, limit: Int)
func filteredRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> ValueBoxFilterResult, limit: Int)
func filteredRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> ValueBoxFilterResult, limit: Int)
func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> Bool, limit: Int)
func scan(_ table: ValueBoxTable, values: (ValueBoxKey, ReadBuffer) -> Bool)
func scan(_ table: ValueBoxTable, keys: (ValueBoxKey) -> Bool)

View File

@ -114,6 +114,7 @@ private final class SettingsItemArguments {
let openPhoneNumberChange: () -> Void
let accountContextAction: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void
let openDevices: () -> Void
let openFilters: () -> Void
init(
sharedContext: SharedAccountContext,
@ -147,7 +148,8 @@ private final class SettingsItemArguments {
keepPhone: @escaping () -> Void,
openPhoneNumberChange: @escaping () -> Void,
accountContextAction: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void,
openDevices: @escaping () -> Void
openDevices: @escaping () -> Void,
openFilters: @escaping () -> Void
) {
self.sharedContext = sharedContext
self.avatarAndNameInfoContext = avatarAndNameInfoContext
@ -181,6 +183,7 @@ private final class SettingsItemArguments {
self.openPhoneNumberChange = openPhoneNumberChange
self.accountContextAction = accountContextAction
self.openDevices = openDevices
self.openFilters = openFilters
}
}
@ -211,6 +214,8 @@ private indirect enum SettingsEntry: ItemListNodeEntry {
case devices(PresentationTheme, UIImage?, String, String)
case filters(PresentationTheme, UIImage?, String, String)
case savedMessages(PresentationTheme, UIImage?, String)
case recentCalls(PresentationTheme, UIImage?, String)
case stickers(PresentationTheme, UIImage?, String, String, [ArchivedStickerPackItem]?)
@ -240,7 +245,7 @@ private indirect enum SettingsEntry: ItemListNodeEntry {
return SettingsSection.accounts.rawValue
case .proxy:
return SettingsSection.proxy.rawValue
case .devices:
case .devices, .filters:
return SettingsSection.media.rawValue
case .savedMessages, .recentCalls, .stickers:
return SettingsSection.media.rawValue
@ -285,30 +290,32 @@ private indirect enum SettingsEntry: ItemListNodeEntry {
return 1006
case .devices:
return 1007
case .notificationsAndSounds:
case .filters:
return 1008
case .privacyAndSecurity:
case .notificationsAndSounds:
return 1009
case .dataAndStorage:
case .privacyAndSecurity:
return 1010
case .themes:
case .dataAndStorage:
return 1011
case .language:
case .themes:
return 1012
case .contentStickers:
case .language:
return 1013
case .contentStickers:
return 1014
#if ENABLE_WALLET
case .wallet:
return 1014
return 1015
#endif
case .passport:
return 1015
case .watch:
return 1016
case .askAQuestion:
case .watch:
return 1017
case .faq:
case .askAQuestion:
return 1018
case .faq:
return 1019
}
}
@ -406,6 +413,12 @@ private indirect enum SettingsEntry: ItemListNodeEntry {
} else {
return false
}
case let .filters(lhsTheme, lhsImage, lhsText, lhsValue):
if case let .filters(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .savedMessages(lhsTheme, lhsImage, lhsText):
if case let .savedMessages(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText {
return true
@ -567,6 +580,10 @@ private indirect enum SettingsEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.openDevices()
})
case let .filters(theme, image, text, value):
return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.openFilters()
})
case let .savedMessages(theme, image, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.openSavedMessages()
@ -635,7 +652,7 @@ private struct SettingsState: Equatable {
var isSearching: Bool
}
private func settingsEntries(account: Account, presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, notifyExceptions: NotificationExceptionsList?, notificationsAuthorizationStatus: AccessType, notificationsWarningSuppressed: Bool, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, privacySettings: AccountPrivacySettings?, hasWallet: Bool, hasPassport: Bool, hasWatchApp: Bool, accountsAndPeers: [(Account, Peer, Int32)], inAppNotificationSettings: InAppNotificationSettings, experimentalUISettings: ExperimentalUISettings, displayPhoneNumberConfirmation: Bool, otherSessionCount: Int, enableQRLogin: Bool) -> [SettingsEntry] {
private func settingsEntries(account: Account, presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, notifyExceptions: NotificationExceptionsList?, notificationsAuthorizationStatus: AccessType, notificationsWarningSuppressed: Bool, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, privacySettings: AccountPrivacySettings?, hasWallet: Bool, hasPassport: Bool, hasWatchApp: Bool, accountsAndPeers: [(Account, Peer, Int32)], inAppNotificationSettings: InAppNotificationSettings, experimentalUISettings: ExperimentalUISettings, displayPhoneNumberConfirmation: Bool, otherSessionCount: Int, enableQRLogin: Bool, enableFilters: Bool) -> [SettingsEntry] {
var entries: [SettingsEntry] = []
if let peer = peerViewMainPeer(view) as? TelegramUser {
@ -688,6 +705,10 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
} else {
entries.append(.devices(presentationData.theme, UIImage(bundleImageName: "Settings/MenuIcons/Sessions")?.precomposed(), presentationData.strings.Settings_Devices, otherSessionCount == 0 ? "" : "\(otherSessionCount + 1)"))
}
if enableFilters {
//TODO:localize
entries.append(.filters(presentationData.theme, UIImage(bundleImageName: "Settings/MenuIcons/SavedMessages")?.precomposed(), "Chat Filters", ""))
}
let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed)
entries.append(.notificationsAndSounds(presentationData.theme, PresentationResourcesSettings.notifications, presentationData.strings.Settings_NotificationsAndSounds, notifyExceptions, notificationsWarning))
@ -925,6 +946,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
let privacySettings = Promise<AccountPrivacySettings?>(nil)
let enableQRLogin = Promise<Bool>()
let enableFilters = Promise<Bool>()
let openFaq: (Promise<ResolvedUrl>, String?) -> Void = { resolvedUrl, customAnchor in
let _ = (contextValue.get()
@ -1221,17 +1243,27 @@ public func settingsController(context: AccountContext, accountManager: AccountM
gesture?.cancel()
}
}, openDevices: {
let _ = (combineLatest(queue: .mainQueue(),
activeSessionsContextAndCount.get(),
enableQRLogin.get()
)
|> take(1)).start(next: { activeSessionsContextAndCount, enableQRLogin in
let (activeSessionsContext, count, webSessionsContext) = activeSessionsContextAndCount
if count == 0 && enableQRLogin {
pushControllerImpl?(AuthDataTransferSplashScreen(context: context, activeSessionsContext: activeSessionsContext))
} else {
pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false))
}
let _ = (contextValue.get()
|> deliverOnMainQueue
|> take(1)).start(next: { context in
let _ = (combineLatest(queue: .mainQueue(),
activeSessionsContextAndCount.get(),
enableQRLogin.get()
)
|> take(1)).start(next: { activeSessionsContextAndCount, enableQRLogin in
let (activeSessionsContext, count, webSessionsContext) = activeSessionsContextAndCount
if count == 0 && enableQRLogin {
pushControllerImpl?(AuthDataTransferSplashScreen(context: context, activeSessionsContext: activeSessionsContext))
} else {
pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false))
}
})
})
}, openFilters: {
let _ = (contextValue.get()
|> deliverOnMainQueue
|> take(1)).start(next: { context in
pushControllerImpl?(chatListFilterPresetListController(context: context, updated: { _ in }))
})
})
@ -1496,7 +1528,21 @@ public func settingsController(context: AccountContext, accountManager: AccountM
}
enableQRLogin.set(enableQRLoginSignal)
let signal = combineLatest(queue: Queue.mainQueue(), contextValue.get(), updatedPresentationData, statePromise.get(), peerView, combineLatest(queue: Queue.mainQueue(), preferences, notifyExceptions.get(), notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), privacySettings.get(), displayPhoneNumberConfirmation.get()), combineLatest(featuredStickerPacks, archivedPacks.get()), combineLatest(hasWallet, hasPassport.get(), hasWatchApp, enableQRLogin.get()), accountsAndPeers.get(), activeSessionsContextAndCount.get())
let enableFiltersSignal = contextValue.get()
|> mapToSignal { context -> Signal<Bool, NoError> in
return context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { view -> Bool in
guard let appConfiguration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration else {
return false
}
let configuration = ChatListFilteringConfiguration(appConfiguration: appConfiguration)
return configuration.isEnabled
}
|> distinctUntilChanged
}
enableFilters.set(enableFiltersSignal)
let signal = combineLatest(queue: Queue.mainQueue(), contextValue.get(), updatedPresentationData, statePromise.get(), peerView, combineLatest(queue: Queue.mainQueue(), preferences, notifyExceptions.get(), notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), privacySettings.get(), displayPhoneNumberConfirmation.get()), combineLatest(featuredStickerPacks, archivedPacks.get()), combineLatest(hasWallet, hasPassport.get(), hasWatchApp, enableQRLogin.get(), enableFilters.get()), accountsAndPeers.get(), activeSessionsContextAndCount.get())
|> map { context, presentationData, state, view, preferencesAndExceptions, featuredAndArchived, hasWalletPassportAndWatch, accountsAndPeers, activeSessionsContextAndCount -> (ItemListControllerState, (ItemListNodeState, Any)) in
let otherSessionCount = activeSessionsContextAndCount.1
@ -1536,8 +1582,8 @@ public func settingsController(context: AccountContext, accountManager: AccountM
pushControllerImpl?(c)
}, getNavigationController: getNavigationControllerImpl, exceptionsList: notifyExceptions.get(), archivedStickerPacks: archivedPacks.get(), privacySettings: privacySettings.get(), hasWallet: hasWallet, activeSessionsContext: activeSessionsContextAndCountSignal |> map { $0.0 } |> distinctUntilChanged(isEqual: { $0 === $1 }), webSessionsContext: activeSessionsContextAndCountSignal |> map { $0.2 } |> distinctUntilChanged(isEqual: { $0 === $1 }))
let (hasWallet, hasPassport, hasWatchApp, enableQRLogin) = hasWalletPassportAndWatch
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: settingsEntries(account: context.account, presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, notifyExceptions: preferencesAndExceptions.1, notificationsAuthorizationStatus: preferencesAndExceptions.2, notificationsWarningSuppressed: preferencesAndExceptions.3, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, privacySettings: preferencesAndExceptions.4, hasWallet: hasWallet, hasPassport: hasPassport, hasWatchApp: hasWatchApp, accountsAndPeers: accountsAndPeers.1, inAppNotificationSettings: inAppNotificationSettings, experimentalUISettings: experimentalUISettings, displayPhoneNumberConfirmation: preferencesAndExceptions.5, otherSessionCount: otherSessionCount, enableQRLogin: enableQRLogin), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up))
let (hasWallet, hasPassport, hasWatchApp, enableQRLogin, enableFilters) = hasWalletPassportAndWatch
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: settingsEntries(account: context.account, presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, notifyExceptions: preferencesAndExceptions.1, notificationsAuthorizationStatus: preferencesAndExceptions.2, notificationsWarningSuppressed: preferencesAndExceptions.3, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, privacySettings: preferencesAndExceptions.4, hasWallet: hasWallet, hasPassport: hasPassport, hasWatchApp: hasWatchApp, accountsAndPeers: accountsAndPeers.1, inAppNotificationSettings: inAppNotificationSettings, experimentalUISettings: experimentalUISettings, displayPhoneNumberConfirmation: preferencesAndExceptions.5, otherSessionCount: otherSessionCount, enableQRLogin: enableQRLogin, enableFilters: enableFilters), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up))
return (controllerState, (listState, arguments))
}

View File

@ -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).start())
let importantBackgroundOperations: [Signal<AccountRunningImportantTasks, NoError>] = [
managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] },

View File

@ -5,7 +5,19 @@ import TelegramApi
import SyncCore
public struct ChatListFilterPeerCategories: OptionSet {
public struct ChatListFilteringConfiguration: Equatable {
public let isEnabled: Bool
public init(appConfiguration: AppConfiguration) {
var isEnabled = false
if let data = appConfiguration.data, let value = data["dialog_filters_enabled"] as? Bool, value {
isEnabled = true
}
self.isEnabled = isEnabled
}
}
public struct ChatListFilterPeerCategories: OptionSet, Hashable {
public var rawValue: Int32
public init(rawValue: Int32) {
@ -93,50 +105,58 @@ extension ChatListFilterPeerCategories {
}
}
public struct ChatListFilter: PostboxCoding, Equatable {
public var id: Int32
public var title: String?
public struct ChatListFilterData: Equatable, Hashable {
public var categories: ChatListFilterPeerCategories
public var excludeMuted: Bool
public var excludeRead: Bool
public var includePeers: [PeerId]
public init(
id: Int32,
title: String?,
categories: ChatListFilterPeerCategories,
excludeMuted: Bool,
excludeRead: Bool,
includePeers: [PeerId]
) {
self.id = id
self.title = title
self.categories = categories
self.excludeMuted = excludeMuted
self.excludeRead = excludeRead
self.includePeers = includePeers
}
}
public struct ChatListFilter: PostboxCoding, Equatable {
public var id: Int32
public var title: String
public var data: ChatListFilterData
public init(
id: Int32,
title: String,
data: ChatListFilterData
) {
self.id = id
self.title = title
self.data = data
}
public init(decoder: PostboxDecoder) {
self.id = decoder.decodeInt32ForKey("id", orElse: 0)
self.title = decoder.decodeOptionalStringForKey("title")
self.categories = ChatListFilterPeerCategories(rawValue: decoder.decodeInt32ForKey("categories", orElse: 0))
self.excludeMuted = decoder.decodeInt32ForKey("excludeMuted", orElse: 0) != 0
self.excludeRead = decoder.decodeInt32ForKey("excludeRead", orElse: 0) != 0
self.includePeers = decoder.decodeInt64ArrayForKey("includePeers").map(PeerId.init)
self.title = decoder.decodeStringForKey("title", 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,
includePeers: decoder.decodeInt64ArrayForKey("includePeers").map(PeerId.init)
)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.id, forKey: "id")
if let title = self.title {
encoder.encodeString(title, forKey: "title")
} else {
encoder.encodeNil(forKey: "title")
}
encoder.encodeInt32(self.categories.rawValue, forKey: "categories")
encoder.encodeInt32(self.excludeMuted ? 1 : 0, forKey: "excludeMuted")
encoder.encodeInt32(self.excludeRead ? 1 : 0, forKey: "excludeRead")
encoder.encodeInt64Array(self.includePeers.map { $0.toInt64() }, forKey: "includePeers")
encoder.encodeString(self.title, forKey: "title")
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.encodeInt64Array(self.data.includePeers.map { $0.toInt64() }, forKey: "includePeers")
}
}
@ -146,36 +166,38 @@ extension ChatListFilter {
case let .dialogFilter(flags, id, title, includePeers):
self.init(
id: id,
title: title.isEmpty ? nil : title,
categories: ChatListFilterPeerCategories(apiFlags: flags),
excludeMuted: (flags & (1 << 11)) != 0,
excludeRead: (flags & (1 << 12)) != 0,
includePeers: includePeers.compactMap { peer -> PeerId? in
switch peer {
case let .inputPeerUser(userId, _):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
case let .inputPeerChat(chatId):
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)
case let .inputPeerChannel(channelId, _):
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)
default:
return nil
title: title,
data: ChatListFilterData(
categories: ChatListFilterPeerCategories(apiFlags: flags),
excludeMuted: (flags & (1 << 11)) != 0,
excludeRead: (flags & (1 << 12)) != 0,
includePeers: includePeers.compactMap { peer -> PeerId? in
switch peer {
case let .inputPeerUser(userId, _):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
case let .inputPeerChat(chatId):
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)
case let .inputPeerChannel(channelId, _):
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)
default:
return nil
}
}
}
)
)
}
}
func apiFilter(transaction: Transaction) -> Api.DialogFilter {
var flags: Int32 = 0
if self.excludeMuted {
if self.data.excludeMuted {
flags |= 1 << 11
}
if self.excludeRead {
if self.data.excludeRead {
flags |= 1 << 12
}
flags |= self.categories.apiFlags
return .dialogFilter(flags: flags, id: self.id, title: self.title ?? "", includePeers: self.includePeers.compactMap { peerId -> Api.InputPeer? in
flags |= self.data.categories.apiFlags
return .dialogFilter(flags: flags, id: self.id, title: self.title, includePeers: self.data.includePeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
})
}
@ -287,46 +309,70 @@ func managedChatListFilters(postbox: Postbox, network: Network) -> Signal<Never,
}
public func replaceRemoteChatListFilters(account: Account) -> Signal<Never, NoError> {
return requestChatListFilters(postbox: account.postbox, network: account.network)
|> `catch` { _ -> Signal<[ChatListFilter], NoError> in
return .complete()
return account.postbox.transaction { transaction -> [ChatListFilter] in
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default
return settings.filters
}
|> mapToSignal { remoteFilters -> Signal<Never, NoError> in
var deleteSignals: [Signal<Never, NoError>] = []
for filter in remoteFilters {
deleteSignals.append(requestUpdateChatListFilter(account: account, id: filter.id, filter: nil)
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
|> ignoreValues)
|> mapToSignal { filters -> Signal<Never, NoError> in
return requestChatListFilters(postbox: account.postbox, network: account.network)
|> `catch` { _ -> Signal<[ChatListFilter], NoError> in
return .complete()
}
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)
|> mapToSignal { remoteFilters -> Signal<Never, NoError> in
var deleteSignals: [Signal<Never, NoError>] = []
for filter in remoteFilters {
if !filters.contains(where: { $0.id == filter.id }) {
deleteSignals.append(requestUpdateChatListFilter(account: account, id: filter.id, filter: nil)
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
|> ignoreValues)
}
}
}
|> mapToSignal { filters -> Signal<Never, NoError> in
var signals: [Signal<Never, NoError>] = []
for (id, filter) in filters {
signals.append(requestUpdateChatListFilter(account: account, id: id, filter: filter)
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)
}
}
|> mapToSignal { filters -> Signal<Never, NoError> in
var signals: [Signal<Never, NoError>] = []
for (id, filter) in filters {
if !remoteFilters.contains(filter) {
signals.append(requestUpdateChatListFilter(account: account, id: id, filter: filter)
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
|> ignoreValues)
}
}
return combineLatest(signals)
|> ignoreValues
}
let reorderFilters: Signal<Never, NoError>
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<Never, NoError> in
return .complete()
}
|> ignoreValues)
} else {
reorderFilters = .complete()
}
return combineLatest(signals)
return combineLatest(
deleteSignals
)
|> ignoreValues
|> then(
addFilters
)
|> then(
reorderFilters
)
}
return combineLatest(
deleteSignals
)
|> ignoreValues
|> then(
addFilters
)
}
}

View File

@ -2,7 +2,7 @@
"images" : [
{
"idiom" : "universal",
"filename" : "ic_list.pdf"
"filename" : "ic_folder.pdf"
}
],
"info" : {

View File

@ -83,7 +83,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf.contactsNode.contactListNode.scrollToTop()
strongSelf.contactsNode.scrollToTop()
}
}
@ -132,33 +132,51 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
private func updateTitle() {
switch self.mode {
case .groupCreation:
let maxCount: Int32 = self.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
let count = self.contactsNode.contactListNode.selectionState?.selectedPeerIndices.count ?? 0
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Compose_NewGroupTitle, counter: "\(count)/\(maxCount)")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = count != 0
case .channelCreation:
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = true
case .peerSelection:
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed))
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = false
case .groupCreation:
let maxCount: Int32 = self.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
let count: Int
switch self.contactsNode.contentNode {
case let .contacts(contactsNode):
count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0
case let .chats(chatsNode):
count = chatsNode.currentState.selectedPeerIds.count
}
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Compose_NewGroupTitle, counter: "\(count)/\(maxCount)")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = count != 0
case .channelCreation:
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = true
case .peerSelection:
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed))
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = false
case .chatSelection:
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.ChatListFilter_AddChatsTitle, counter: "")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed))
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = false
}
}
override func loadDisplayNode() {
self.displayNode = ContactMultiselectionControllerNode(context: self.context, mode: self.mode, options: self.options, filters: filters)
self._listReady.set(self.contactsNode.contactListNode.ready)
switch self.contactsNode.contentNode {
case let .contacts(contactsNode):
self._listReady.set(contactsNode.ready)
case let .chats(chatsNode):
self._listReady.set(chatsNode.ready)
}
self.contactsNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: true, completion: nil)
@ -174,25 +192,47 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
var displayCountAlert = false
var selectionState: ContactListNodeGroupSelectionState?
strongSelf.contactsNode.contactListNode.updateSelectionState { state in
if let state = state {
var updatedState = state.withToggledPeerId(.peer(peer.id))
if updatedState.selectedPeerIndices[.peer(peer.id)] == nil {
switch strongSelf.contactsNode.contentNode {
case let .contacts(contactsNode):
contactsNode.updateSelectionState { state in
if let state = state {
var updatedState = state.withToggledPeerId(.peer(peer.id))
if updatedState.selectedPeerIndices[.peer(peer.id)] == nil {
removedTokenId = peer.id
} else {
if updatedState.selectedPeerIndices.count >= maxRegularCount {
displayCountAlert = true
updatedState = updatedState.withToggledPeerId(.peer(peer.id))
} else {
addedToken = EditableTokenListToken(id: peer.id, title: peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder))
}
}
updatedCount = updatedState.selectedPeerIndices.count
selectionState = updatedState
return updatedState
} else {
return nil
}
}
case let .chats(chatsNode):
chatsNode.updateState { state in
var state = state
if state.selectedPeerIds.contains(peer.id) {
state.selectedPeerIds.remove(peer.id)
removedTokenId = peer.id
} else {
if updatedState.selectedPeerIndices.count >= maxRegularCount {
displayCountAlert = true
updatedState = updatedState.withToggledPeerId(.peer(peer.id))
} else {
addedToken = EditableTokenListToken(id: peer.id, title: peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder))
}
addedToken = EditableTokenListToken(id: peer.id, title: peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder))
state.selectedPeerIds.insert(peer.id)
}
updatedCount = state.selectedPeerIds.count
var updatedState = ContactListNodeGroupSelectionState()
for peerId in state.selectedPeerIds {
updatedState = updatedState.withToggledPeerId(.peer(peerId))
}
updatedCount = updatedState.selectedPeerIndices.count
selectionState = updatedState
return updatedState
} else {
return nil
return state
}
break
}
if let searchResultsNode = strongSelf.contactsNode.searchResultsNode {
searchResultsNode.updateSelectionState { _ in
@ -202,7 +242,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
if let updatedCount = updatedCount {
switch strongSelf.mode {
case .groupCreation, .peerSelection:
case .groupCreation, .peerSelection, .chatSelection:
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0
case .channelCreation:
break
@ -212,7 +252,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
case .groupCreation:
let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)")
case .peerSelection, .channelCreation:
case .peerSelection, .channelCreation, .chatSelection:
break
}
}
@ -238,19 +278,39 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
var removedTokenId: AnyHashable?
var selectionState: ContactListNodeGroupSelectionState?
strongSelf.contactsNode.contactListNode.updateSelectionState { state in
if let state = state {
let updatedState = state.withToggledPeerId(peerId)
if updatedState.selectedPeerIndices[peerId] == nil {
if case let .peer(peerId) = peerId {
switch strongSelf.contactsNode.contentNode {
case let .contacts(contactsNode):
contactsNode.updateSelectionState { state in
if let state = state {
let updatedState = state.withToggledPeerId(peerId)
if updatedState.selectedPeerIndices[peerId] == nil {
if case let .peer(peerId) = peerId {
removedTokenId = peerId
}
}
updatedCount = updatedState.selectedPeerIndices.count
selectionState = updatedState
return updatedState
} else {
return nil
}
}
case let .chats(chatsNode):
chatsNode.updateState { state in
var state = state
if case let .peer(peerIdValue) = peerId {
if state.selectedPeerIds.contains(peerIdValue) {
state.selectedPeerIds.remove(peerIdValue)
removedTokenId = peerId
}
}
updatedCount = updatedState.selectedPeerIndices.count
updatedCount = state.selectedPeerIds.count
var updatedState = ContactListNodeGroupSelectionState()
for peerId in state.selectedPeerIds {
updatedState = updatedState.withToggledPeerId(.peer(peerId))
}
selectionState = updatedState
return updatedState
} else {
return nil
return state
}
}
if let searchResultsNode = strongSelf.contactsNode.searchResultsNode {
@ -261,7 +321,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
if let updatedCount = updatedCount {
switch strongSelf.mode {
case .groupCreation, .peerSelection:
case .groupCreation, .peerSelection, .chatSelection:
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0
case .channelCreation:
break
@ -270,7 +330,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
case .groupCreation:
let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)")
case .peerSelection, .channelCreation:
case .peerSelection, .channelCreation, .chatSelection:
break
}
}
@ -290,7 +350,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.contactsNode.contactListNode.enableUpdates = true
switch self.contactsNode.contentNode {
case let .contacts(contactsNode):
contactsNode.enableUpdates = true
case .chats:
break
}
}
override func viewDidAppear(_ animated: Bool) {
@ -307,7 +372,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.contactsNode.contactListNode.enableUpdates = false
switch self.contactsNode.contentNode {
case let .contacts(contactsNode):
contactsNode.enableUpdates = false
case .chats:
break
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -323,11 +393,18 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
@objc func rightNavigationButtonPressed() {
var peerIds: [ContactListPeerId] = []
self.contactsNode.contactListNode.updateSelectionState { state in
if let state = state {
peerIds = Array(state.selectedPeerIndices.keys)
switch self.contactsNode.contentNode {
case let .contacts(contactsNode):
contactsNode.updateSelectionState { state in
if let state = state {
peerIds = Array(state.selectedPeerIndices.keys)
}
return state
}
case let .chats(chatsNode):
for peerId in chatsNode.currentState.selectedPeerIds {
peerIds.append(.peer(peerId))
}
return state
}
self._result.set(.single(peerIds))
}

View File

@ -9,6 +9,7 @@ import TelegramPresentationData
import MergeLists
import AccountContext
import ContactListUI
import ChatListUI
private struct SearchResultEntry: Identifiable {
let index: Int
@ -27,8 +28,22 @@ private struct SearchResultEntry: Identifiable {
}
}
enum ContactMultiselectionContentNode {
case contacts(ContactListNode)
case chats(ChatListNode)
var node: ASDisplayNode {
switch self {
case let .contacts(contacts):
return contacts
case let .chats(chats):
return chats
}
}
}
final class ContactMultiselectionControllerNode: ASDisplayNode {
let contactListNode: ContactListNode
let contentNode: ContactMultiselectionContentNode
let tokenListNode: EditableTokenListNode
var searchResultsNode: ContactListNode?
@ -53,7 +68,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
let placeholder: String
var placeholder: String
var includeChatList = false
switch mode {
case let .peerSelection(_, searchGroups, searchChannels):
@ -67,7 +82,13 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
}
self.contactListNode = ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList)), filters: filters, selectionState: ContactListNodeGroupSelectionState())
if case .chatSelection = mode {
placeholder = self.presentationData.strings.Common_Search
self.contentNode = .chats(ChatListNode(context: context, groupId: .root, previewing: false, controlsHistoryPreload: false, mode: .peers(filter: [.excludeSavedMessages], isSelecting: true), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true))
} else {
self.contentNode = .contacts(ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList)), filters: filters, selectionState: ContactListNodeGroupSelectionState()))
}
self.tokenListNode = EditableTokenListNode(theme: EditableTokenListNodeTheme(backgroundColor: self.presentationData.theme.rootController.navigationBar.backgroundColor, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, selectedTextColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder)
super.init()
@ -78,11 +99,18 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.addSubnode(self.contactListNode)
self.addSubnode(self.contentNode.node)
self.addSubnode(self.tokenListNode)
self.contactListNode.openPeer = { [weak self] peer in
self?.openPeer?(peer)
switch self.contentNode {
case let .contacts(contactsNode):
contactsNode.openPeer = { [weak self] peer in
self?.openPeer?(peer)
}
case let .chats(chatsNode):
chatsNode.peerSelected = { [weak self] peer, _, _ in
self?.openPeer?(.peer(peer: peer, isGlobal: false, participantCount: nil))
}
}
let searchText = ValuePromise<String>()
@ -102,9 +130,14 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
} else {
if strongSelf.searchResultsNode == nil {
var selectionState: ContactListNodeGroupSelectionState?
strongSelf.contactListNode.updateSelectionState { state in
selectionState = state
return state
switch strongSelf.contentNode {
case let .contacts(contactsNode):
contactsNode.updateSelectionState { state in
selectionState = state
return state
}
case .chats:
break
}
var searchChatList = false
var searchGroups = false
@ -113,6 +146,10 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
searchChatList = peerSelection.searchChatList
searchGroups = peerSelection.searchGroups
searchChannels = peerSelection.searchChannels
} else if case .chatSelection = mode {
searchChatList = true
searchGroups = true
searchChannels = true
}
let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false, searchGroups: searchGroups, searchChannels: searchChannels)), filters: filters, selectionState: selectionState)
searchResultsNode.openPeer = { peer in
@ -136,7 +173,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
strongSelf.searchResultsReadyDisposable.set((searchResultsNode.ready |> deliverOnMainQueue).start(next: { _ in
if let strongSelf = self, let searchResultsNode = strongSelf.searchResultsNode {
strongSelf.insertSubnode(searchResultsNode, aboveSubnode: strongSelf.contactListNode)
strongSelf.insertSubnode(searchResultsNode, aboveSubnode: strongSelf.contentNode.node)
}
}))
}
@ -167,6 +204,15 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
}
func scrollToTop() {
switch self.contentNode {
case let .contacts(contactsNode):
contactsNode.scrollToTop()
case let .chats(chatsNode):
chatsNode.scrollToPosition(.top)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight)
@ -183,8 +229,15 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
insets.top += tokenListHeight
headerInsets.top += tokenListHeight
self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, transition: transition)
self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size)
switch self.contentNode {
case let .contacts(contactsNode):
contactsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, transition: transition)
case let .chats(chatsNode):
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve)
chatsNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
}
self.contentNode.node.frame = CGRect(origin: CGPoint(), size: layout.size)
if let searchResultsNode = self.searchResultsNode {
searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, transition: transition)

View File

@ -509,7 +509,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
override func didLoad() {
super.didLoad()
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] in
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, let currentPaneKey = strongSelf.currentPaneKey, let availablePanes = strongSelf.currentParams?.data?.availablePanes, let index = availablePanes.index(of: currentPaneKey) else {
return []
}

View File

@ -85,7 +85,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.segmentedControlNode = nil
}
self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, controlsHistoryPreload: false, mode: .peers(filter: filter), theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, controlsHistoryPreload: false, mode: .peers(filter: filter, isSelecting: false), theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
super.init()
@ -99,8 +99,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self?.requestActivateSearch?()
}
self.chatListNode.peerSelected = { [weak self] peerId, _, _ in
self?.requestOpenPeer?(peerId)
self.chatListNode.peerSelected = { [weak self] peer, _, _ in
self?.requestOpenPeer?(peer.id)
}
self.chatListNode.disabledPeerSelected = { [weak self] peer in

View File

@ -448,12 +448,12 @@ public final class WalletStrings: Equatable {
public var Wallet_SecureStorageReset_Title: String { return self._s[218]! }
public var Wallet_Receive_CommentHeader: String { return self._s[219]! }
public var Wallet_Info_ReceiveGrams: String { return self._s[220]! }
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
}
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)