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.ViewsBySourceTitle" = "VIEWS BY SOURCE";
"Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE"; "Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE";
"Stats.LanguagesTitle" = "LANGUAGES"; "Stats.LanguagesTitle" = "LANGUAGES";
"ChatListFilter.AddChatsTitle" = "Add Chats";

View File

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

View File

@ -23,19 +23,26 @@ import LocalizedPeerData
import TelegramIntents import TelegramIntents
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if listNode.scroller.isDragging {
return false
}
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 { if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
let scrollToItem: ListViewScrollToItem let scrollToItem: ListViewScrollToItem
let targetProgress: CGFloat let targetProgress: CGFloat
let offset: CGFloat
if searchNode.expansionProgress < 0.6 { if searchNode.expansionProgress < 0.6 {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: true, curve: .Default(duration: nil), directionHint: .Up) scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: true, curve: .Default(duration: nil), directionHint: .Up)
targetProgress = 0.0 targetProgress = 0.0
offset = navigationBarSearchContentHeight
} else { } else {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up) scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up)
targetProgress = 1.0 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 return true
} else if searchNode.expansionProgress == 1.0 { } else if searchNode.expansionProgress == 1.0 {
var sortItemNode: ListViewItemNode? var sortItemNode: ListViewItemNode?
@ -138,13 +145,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
private var searchContentNode: NavigationBarSearchContentNode? private var searchContentNode: NavigationBarSearchContentNode?
private let tabContainerNode: ChatListFilterTabContainerNode private let tabContainerNode: ChatListFilterTabContainerNode
private var tabContainerData: ([ChatListFilterTabEntry], ChatListFilterTabEntryId)? private var tabContainerData: [ChatListFilterTabEntry]?
private let chatListFilterValue = Promise<ChatListFilter?>()
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
if self.isNodeLoaded { 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 { if strongSelf.chatListDisplayNode.searchDisplayController != nil {
strongSelf.deactivateSearch(animated: true) strongSelf.deactivateSearch(animated: true)
} else { } else {
switch strongSelf.chatListDisplayNode.chatListNode.visibleContentOffset() { switch strongSelf.chatListDisplayNode.containerNode.currentItemNode.visibleContentOffset() {
case .none, .unknown: case .none, .unknown:
if let searchContentNode = strongSelf.searchContentNode { if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true) searchContentNode.updateExpansionProgress(1.0, animated: true)
} }
strongSelf.chatListDisplayNode.chatListNode.scrollToPosition(.top) strongSelf.chatListDisplayNode.containerNode.currentItemNode.scrollToPosition(.top)
case let .known(offset): case let .known(offset):
if offset <= navigationBarSearchContentHeight + 1.0 { if offset <= navigationBarSearchContentHeight + 1.0 {
strongSelf.tabContainerNode.tabSelected?(.all) strongSelf.tabContainerNode.tabSelected?(.all)
@ -245,24 +250,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
if let searchContentNode = strongSelf.searchContentNode { if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true) 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]) let hasProxy = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.proxySettings])
|> map { sharedData -> (Bool, Bool) in |> map { sharedData -> (Bool, Bool) in
@ -287,22 +279,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
context.account.networkState, context.account.networkState,
hasProxy, hasProxy,
passcode, passcode,
self.chatListDisplayNode.chatListNode.state self.chatListDisplayNode.containerNode.currentItemState
).start(next: { [weak self] networkState, proxy, passcode, state in ).start(next: { [weak self] networkState, proxy, passcode, state in
if let strongSelf = self { if let strongSelf = self {
let defaultTitle: String let defaultTitle: String
if strongSelf.groupId == .root { 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 { } else {
defaultTitle = strongSelf.presentationData.strings.ChatList_ArchivedChatsTitle defaultTitle = strongSelf.presentationData.strings.ChatList_ArchivedChatsTitle
} }
if state.editing { if state.editing {
if strongSelf.groupId == .root && strongSelf.filter == nil { if strongSelf.groupId == .root {
strongSelf.navigationItem.rightBarButtonItem = nil strongSelf.navigationItem.rightBarButtonItem = nil
} }
@ -312,12 +299,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
var isRoot = false var isRoot = false
if case .root = strongSelf.groupId { if case .root = strongSelf.groupId {
isRoot = true isRoot = true
if strongSelf.filter == nil {
let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed)) 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 rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose
strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem
} }
}
let (hasProxy, connectsViaProxy) = proxy let (hasProxy, connectsViaProxy) = proxy
let (isPasscodeSet, isManuallyLocked) = passcode let (isPasscodeSet, isManuallyLocked) = passcode
@ -420,30 +405,39 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} }
if self.filter == nil { 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([ let preferencesKey: PostboxViewKey = .preferences(keys: Set([
ApplicationSpecificPreferencesKeys.chatListFilterSettings ApplicationSpecificPreferencesKeys.chatListFilterSettings
])) ]))
let filterItems = chatListFilterItems(context: context) 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(), self.filterDisposable = (combineLatest(queue: .mainQueue(),
context.account.postbox.combinedView(keys: [ context.account.postbox.combinedView(keys: [
preferencesKey preferencesKey
]), ]),
filterItems, filterItems
self.chatListFilterValue.get() |> map { $0?.id } |> distinctUntilChanged
) )
|> deliverOnMainQueue).start(next: { [weak self] combinedView, filterItems, selectedFilter in |> deliverOnMainQueue).start(next: { [weak self] combinedView, countAndFilterItems in
guard let strongSelf = self else { guard let strongSelf = self else {
return 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 var filterSettings: ChatListFilterSettings = .default
if let preferencesView = combinedView.views[preferencesKey] as? PreferencesView { if let preferencesView = combinedView.views[preferencesKey] as? PreferencesView {
if let value = preferencesView.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings] as? ChatListFilterSettings { if let value = preferencesView.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings] as? ChatListFilterSettings {
@ -458,12 +452,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
var wasEmpty = false var wasEmpty = false
if let tabContainerData = strongSelf.tabContainerData { if let tabContainerData = strongSelf.tabContainerData {
wasEmpty = tabContainerData.0.count <= 1 wasEmpty = tabContainerData.count <= 1
} else { } else {
wasEmpty = true wasEmpty = true
} }
let selectedEntryId: ChatListFilterTabEntryId = selectedFilter.flatMap { .filter($0) } ?? .all let selectedEntryId = strongSelf.chatListDisplayNode.containerNode.currentItemFilter
strongSelf.tabContainerData = (resolvedItems, selectedEntryId) 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 let isEmpty = resolvedItems.count <= 1
@ -479,7 +479,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
strongSelf.containerLayoutUpdated(layout, transition: .immediate) strongSelf.containerLayoutUpdated(layout, transition: .immediate)
(strongSelf.parent as? TabBarController)?.updateLayout() (strongSelf.parent as? TabBarController)?.updateLayout()
} else { } 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 { guard let strongSelf = self else {
return return
} }
let previousFilter = strongSelf.chatListDisplayNode.chatListNode.chatListFilter let previousFilter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter
let updatedFilter: ChatListFilter? let updatedFilter: ChatListFilter?
switch id { switch id {
case .all: case .all:
@ -518,26 +518,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
updatedFilter = nil updatedFilter = nil
} }
} }
if previousFilter?.id != updatedFilter?.id { strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: updatedFilter.flatMap { .filter($0.id) } ?? .all)
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)
}) })
} }
@ -546,12 +527,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} }
self.tabContainerNode.contextGesture = { [weak self] id, sourceNode, gesture in self.tabContainerNode.contextGesture = { [weak self] id, sourceNode, gesture in
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: { [weak self] filters in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Edit, icon: { _ in items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Edit, icon: { theme in
return nil return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
}, action: { c, f in }, action: { c, f in
c.dismiss(completion: { c.dismiss(completion: {
guard let strongSelf = self else { guard let strongSelf = self else {
@ -577,10 +566,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}) })
}) })
}))) })))
if let chatListFilter = strongSelf.chatListDisplayNode.chatListNode.chatListFilter, chatListFilter.includePeers.count < 100 { if let filter = filters.first(where: { $0.id == id }), filter.data.includePeers.count < 100 {
//TODO:localization //TODO:localization
items.append(.action(ContextMenuActionItem(text: "Add Chats", icon: { _ in items.append(.action(ContextMenuActionItem(text: "Add Chats", icon: { theme in
return nil return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { c, f in }, action: { c, f in
c.dismiss(completion: { c.dismiss(completion: {
guard let strongSelf = self else { guard let strongSelf = self else {
@ -606,10 +595,45 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}) })
}) })
}))) })))
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) 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) 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) self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.DialogList_SearchLabel)
var editing = false let editing = self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing
self.chatListDisplayNode.chatListNode.updateState { state in
editing = state.editing
return state
}
let editItem: UIBarButtonItem let editItem: UIBarButtonItem
if editing { if editing {
editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) 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() { 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.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 self.chatListDisplayNode.navigationBar = self.navigationBar
@ -684,37 +703,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self?.deactivateSearch(animated: true) self?.deactivateSearch(animated: true)
} }
self.chatListDisplayNode.chatListNode.activateSearch = { [weak self] in self.chatListDisplayNode.containerNode.activateSearch = { [weak self] in
self?.activateSearch() self?.activateSearch()
} }
self.chatListDisplayNode.chatListNode.presentAlert = { [weak self] text in self.chatListDisplayNode.containerNode.presentAlert = { [weak self] text in
if let strongSelf = self { 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?.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 { if let strongSelf = self {
self?.present(c, in: .window(.root)) 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 { guard let strongSelf = self else {
return return
} }
strongSelf.toggleArchivedFolderHiddenByDefault() strongSelf.toggleArchivedFolderHiddenByDefault()
} }
self.chatListDisplayNode.chatListNode.deletePeerChat = { [weak self] peerId in self.chatListDisplayNode.containerNode.deletePeerChat = { [weak self] peerId in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.deletePeerChat(peerId: peerId) 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 strongSelf = self {
if let navigationController = strongSelf.navigationController as? NavigationController { if let navigationController = strongSelf.navigationController as? NavigationController {
if isAd { if isAd {
@ -738,37 +757,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
scrollToEndIfExists = true 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 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.chatListNode.clearHighlightAnimated(true) 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 strongSelf = self {
if let navigationController = strongSelf.navigationController as? NavigationController { if let navigationController = strongSelf.navigationController as? NavigationController {
let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupId, controlsHistoryPreload: false, enableDebugActions: false) let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupId, controlsHistoryPreload: false, enableDebugActions: false)
chatListController.navigationPresentation = .master chatListController.navigationPresentation = .master
navigationController.pushViewController(chatListController) 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 { guard let strongSelf = self else {
return return
} }
if group { if group {
strongSelf.archiveChats(peerIds: [peerId]) strongSelf.archiveChats(peerIds: [peerId])
} else { } 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: { let _ = updatePeerGroupIdInteractively(postbox: strongSelf.context.account.postbox, peerId: peerId, groupId: group ? Namespaces.PeerGroup.archive : .root).start(completed: {
guard let strongSelf = self else { guard let strongSelf = self else {
return 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: { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeerId), subject: .message(messageId), purposefulAction: {
self?.deactivateSearch(animated: false) self?.deactivateSearch(animated: false)
}, scrollToEndIfExists: scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [])) }, 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 strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), purposefulAction: { [weak self] in
self?.deactivateSearch(animated: false) self?.deactivateSearch(animated: false)
}, scrollToEndIfExists: scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [])) }, 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) 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 { if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode, let validLayout = strongSelf.validLayout {
var offset = offset var offset = offset
if validLayout.inVoiceOver { 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 { if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
return fixListNodeScrolling(listView, searchNode: searchContentNode) return fixListNodeScrolling(listView, searchNode: searchContentNode)
} else { } else {
@ -897,7 +916,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else { guard let strongSelf = self else {
return 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 })) strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in }))
} else { } else {
strongSelf.composePressed() strongSelf.composePressed()
@ -908,7 +927,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self?.toolbarActionSelected(action: action) 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 { guard let strongSelf = self else {
gesture?.cancel() gesture?.cancel()
return return
@ -940,7 +959,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} }
let context = self.context 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 |> map { state -> Set<PeerId>? in
if !state.editing { if !state.editing {
return nil 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 { } else {
if let (options, peerIds) = peerIdsAndOptions { if let (options, peerIds) = peerIdsAndOptions {
@ -1006,7 +1025,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
strongSelf.setToolbar(toolbar, transition: .animated(duration: 0.3, curve: .easeInOut)) 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() 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 { guard let strongSelf = self else {
return return
} }
@ -1145,7 +1164,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.deactivateSearch(animated: false) self.deactivateSearch(animated: false)
} }
self.chatListDisplayNode.chatListNode.clearHighlightAnimated(true) self.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
} }
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -1161,12 +1180,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
tabContainerOffset += 44.0 + 44.0 + 44.0 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))) 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: NavigationBar.defaultSecondaryContentHeight), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.tabContainerData?.1, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) 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 { if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver {
searchContentNode.updateListVisibleContentOffset(.known(0.0)) 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) 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.searchContentNode?.setIsEnabled(false, animated: true)
self.chatListDisplayNode.chatListNode.updateState { state in self.chatListDisplayNode.containerNode.updateState { state in
var state = state var state = state
state.editing = true state.editing = true
state.peerIdWithRevealedOptions = nil 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.navigationController as? NavigationController)?.updateMasterDetailsBlackout(nil, transition: .animated(duration: 0.4, curve: .spring))
self.searchContentNode?.setIsEnabled(true, animated: true) self.searchContentNode?.setIsEnabled(true, animated: true)
self.chatListDisplayNode.chatListNode.updateState { state in self.chatListDisplayNode.containerNode.updateState { state in
var state = state var state = state
state.editing = false state.editing = false
state.peerIdWithRevealedOptions = nil state.peerIdWithRevealedOptions = nil
@ -1217,7 +1236,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
public func activateSearch() { public func activateSearch() {
if self.displayNavigationBar { if self.displayNavigationBar {
let _ = (self.chatListDisplayNode.chatListNode.contentsReady let _ = (self.chatListDisplayNode.containerNode.currentItemNode.contentsReady
|> take(1) |> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in |> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -1303,10 +1322,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return nil 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? 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 { if let itemNode = itemNode as? ChatListItemNode, itemNode.frame.contains(listLocation), !itemNode.isDisplayingRevealedOptions {
selectedNode = itemNode selectedNode = itemNode
} }
@ -1346,12 +1365,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
chatController.updatePresentationMode(.standard(previewing: false)) chatController.updatePresentationMode(.standard(previewing: false))
if let navigationController = self.navigationController as? NavigationController { 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.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 { } else if let chatListController = viewControllerToCommit as? ChatListController {
if let navigationController = self.navigationController as? NavigationController { if let navigationController = self.navigationController as? NavigationController {
navigationController.pushViewController(chatListController, animated: false, completion: {}) 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] = [ let inputShortcuts: [KeyShortcut] = [
KeyShortcut(title: strings.KeyCommand_JumpToPreviousChat, input: UIKeyCommand.inputUpArrow, modifiers: [.alternate], action: { [weak self] in KeyShortcut(title: strings.KeyCommand_JumpToPreviousChat, input: UIKeyCommand.inputUpArrow, modifiers: [.alternate], action: { [weak self] in
if let strongSelf = self { 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 KeyShortcut(title: strings.KeyCommand_JumpToNextChat, input: UIKeyCommand.inputDownArrow, modifiers: [.alternate], action: { [weak self] in
if let strongSelf = self { 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 KeyShortcut(title: strings.KeyCommand_JumpToPreviousUnreadChat, input: UIKeyCommand.inputUpArrow, modifiers: [.alternate, .shift], action: { [weak self] in
if let strongSelf = self { 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 KeyShortcut(title: strings.KeyCommand_JumpToNextUnreadChat, input: UIKeyCommand.inputDownArrow, modifiers: [.alternate, .shift], action: { [weak self] in
if let strongSelf = self { 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 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 let openChat: (Int) -> Void = { [weak self] index in
if let strongSelf = self { if let strongSelf = self {
if index == 0 { if index == 0 {
strongSelf.chatListDisplayNode.chatListNode.selectChat(.peerId(strongSelf.context.account.peerId)) strongSelf.chatListDisplayNode.containerNode.currentItemNode.selectChat(.peerId(strongSelf.context.account.peerId))
} else { } 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) { 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 { if case .left = action {
let signal: Signal<Void, NoError> let signal: Signal<Void, NoError>
let context = self.context let context = self.context
@ -1433,7 +1452,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} else { } else {
let groupId = self.groupId let groupId = self.groupId
signal = self.context.account.postbox.transaction { transaction -> Void in 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 let _ = (signal
@ -1450,7 +1469,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return return
} }
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state var state = state
for peerId in peerIds { for peerId in peerIds {
state.pendingRemovalPeerIds.insert(peerId) state.pendingRemovalPeerIds.insert(peerId)
@ -1494,15 +1513,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|> deliverOnMainQueue).start() |> deliverOnMainQueue).start()
return true return true
} else if value == .undo { } else if value == .undo {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds.first!) strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!)
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state var state = state
for peerId in peerIds { for peerId in peerIds {
state.pendingRemovalPeerIds.remove(peerId) state.pendingRemovalPeerIds.remove(peerId)
} }
return state return state
}) })
self?.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds.first!) self?.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!)
return true return true
} }
return false return false
@ -1526,7 +1545,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.archiveChats(peerIds: Array(peerIds)) self.archiveChats(peerIds: Array(peerIds))
} else { } else {
if !peerIds.isEmpty { 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 let _ = (self.context.account.postbox.transaction { transaction -> Void in
for peerId in peerIds { for peerId in peerIds {
updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root) updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root)
@ -1536,7 +1555,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
strongSelf.donePressed() strongSelf.donePressed()
}) })
} }
@ -1559,7 +1578,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.chatListDisplayNode.chatListNode.updateState { state in strongSelf.chatListDisplayNode.containerNode.updateState { state in
var state = state var state = state
if value { if value {
state.archiveShouldBeTemporaryRevealed = false state.archiveShouldBeTemporaryRevealed = false
@ -1677,7 +1696,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state var state = state
state.pendingClearHistoryPeerIds.insert(peer.peerId) state.pendingClearHistoryPeerIds.insert(peer.peerId)
return state return state
@ -1698,7 +1717,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state var state = state
state.pendingClearHistoryPeerIds.remove(peer.peerId) state.pendingClearHistoryPeerIds.remove(peer.peerId)
return state return state
@ -1706,7 +1725,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}) })
return true return true
} else if value == .undo { } else if value == .undo {
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state var state = state
state.pendingClearHistoryPeerIds.remove(peer.peerId) state.pendingClearHistoryPeerIds.remove(peer.peerId)
return state return state
@ -1870,7 +1889,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return return
} }
let postbox = self.context.account.postbox 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) let _ = (ApplicationSpecificNotice.incrementArchiveChatTips(accountManager: self.context.sharedContext.accountManager, count: 1)
|> deliverOnMainQueue).start(next: { [weak self] previousHintCount in |> deliverOnMainQueue).start(next: { [weak self] previousHintCount in
let _ = (postbox.transaction { transaction -> Void in let _ = (postbox.transaction { transaction -> Void in
@ -1882,7 +1901,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
for peerId in peerIds { for peerId in peerIds {
deleteSendMessageIntents(peerId: peerId) deleteSendMessageIntents(peerId: peerId)
@ -1893,7 +1912,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return false return false
} }
if value == .undo { if value == .undo {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds[0]) strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds[0])
let _ = (postbox.transaction { transaction -> Void in let _ = (postbox.transaction { transaction -> Void in
for peerId in peerIds { for peerId in peerIds {
updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root) updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root)
@ -1903,7 +1922,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
}) })
return true return true
} else { } else {
@ -1949,13 +1968,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} }
let peerId = peer.peerId let peerId = peer.peerId
self.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId) self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerId)
self.chatListDisplayNode.chatListNode.updateState({ state in self.chatListDisplayNode.containerNode.updateState({ state in
var state = state var state = state
state.pendingRemovalPeerIds.insert(peer.peerId) state.pendingRemovalPeerIds.insert(peer.peerId)
return state return state
}) })
self.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
let statusText: String let statusText: String
if let channel = chatPeer as? TelegramChannel { if let channel = chatPeer as? TelegramChannel {
if deleteGloballyIfPossible { if deleteGloballyIfPossible {
@ -1999,7 +2018,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return false return false
} }
if value == .commit { if value == .commit {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId) strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerId)
if let channel = chatPeer as? TelegramChannel { if let channel = chatPeer as? TelegramChannel {
strongSelf.context.peerChannelMemberCategoriesContextsManager.externallyRemoved(peerId: channel.id, memberId: strongSelf.context.account.peerId) 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 { guard let strongSelf = self else {
return return
} }
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state var state = state
state.pendingRemovalPeerIds.remove(peer.peerId) state.pendingRemovalPeerIds.remove(peer.peerId)
return state return state
}) })
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
deleteSendMessageIntents(peerId: peerId) deleteSendMessageIntents(peerId: peerId)
}) })
completion() completion()
return true return true
} else if value == .undo { } else if value == .undo {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId) strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerId)
strongSelf.chatListDisplayNode.chatListNode.updateState({ state in strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state var state = state
state.pendingRemovalPeerIds.remove(peer.peerId) state.pendingRemovalPeerIds.remove(peer.peerId)
return state return state
}) })
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
return true return true
} }
return false 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) { override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
let _ = (combineLatest(queue: .mainQueue(), let _ = (combineLatest(queue: .mainQueue(),
self.context.account.postbox.transaction { transaction -> [ChatListFilter] in 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 { if !presetList.isEmpty {
items.append(.separator) items.append(.separator)
@ -2135,25 +2137,25 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
case privateChats case privateChats
} }
let filterType: ChatListFilterType let filterType: ChatListFilterType
if preset.includePeers.isEmpty { if preset.data.includePeers.isEmpty {
if preset.categories == .all { if preset.data.categories == .all {
if preset.excludeRead { if preset.data.excludeRead {
filterType = .unread filterType = .unread
} else if preset.excludeMuted { } else if preset.data.excludeMuted {
filterType = .unmuted filterType = .unmuted
} else { } else {
filterType = .generic filterType = .generic
} }
} else { } else {
if preset.categories == .channels { if preset.data.categories == .channels {
filterType = .channels filterType = .channels
} else if preset.categories.isSubset(of: [.publicGroups, .privateGroups]) { } else if preset.data.categories.isSubset(of: [.publicGroups, .privateGroups]) {
filterType = .groups filterType = .groups
} else if preset.categories == .bots { } else if preset.data.categories == .bots {
filterType = .bots filterType = .bots
} else if preset.categories == .secretChats { } else if preset.data.categories == .secretChats {
filterType = .secretChats filterType = .secretChats
} else if preset.categories == .privateChats { } else if preset.data.categories == .privateChats {
filterType = .privateChats filterType = .privateChats
} else { } else {
filterType = .generic filterType = .generic
@ -2203,6 +2205,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) 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 { private final class ChatListTabBarContextExtractedContentSource: ContextExtractedContentSource {

View File

@ -26,19 +26,579 @@ private final class ChatListControllerNodeView: UITracingLayerView, PreviewingHo
weak var controller: ChatListControllerImpl? weak var controller: ChatListControllerImpl?
} }
private struct TestItem: Comparable, Identifiable { enum ChatListContainerNodeFilter: Equatable {
var value: Int case all
var version: Int case filter(ChatListFilter)
var stableId: Int { var id: ChatListFilterTabEntryId {
return self.value switch self {
case .all:
return .all
case let .filter(filter):
return .filter(filter.id)
}
} }
static func <(lhs: TestItem, rhs: TestItem) -> Bool { var filter: ChatListFilter? {
if lhs.version != rhs.version { switch self {
return lhs.version < rhs.version 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 let groupId: PeerGroupId
private var presentationData: PresentationData private var presentationData: PresentationData
private var chatListEmptyNodeContainer: ChatListEmptyNodeContainer let containerNode: ChatListContainerNode
private var chatListEmptyIndicator: ActivityIndicator?
let chatListNode: ChatListNode
var navigationBar: NavigationBar? var navigationBar: NavigationBar?
weak var controller: ChatListControllerImpl? weak var controller: ChatListControllerImpl?
@ -78,8 +636,13 @@ final class ChatListControllerNode: ASDisplayNode {
self.groupId = groupId self.groupId = groupId
self.presentationData = presentationData self.presentationData = presentationData
self.chatListEmptyNodeContainer = ChatListEmptyNodeContainer(theme: presentationData.theme, strings: presentationData.strings) var filterBecameEmpty: ((ChatListFilter?) -> Void)?
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 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 self.controller = controller
@ -91,35 +654,24 @@ final class ChatListControllerNode: ASDisplayNode {
self.backgroundColor = presentationData.theme.chatList.backgroundColor self.backgroundColor = presentationData.theme.chatList.backgroundColor
self.addSubnode(self.chatListNode) self.addSubnode(self.containerNode)
self.addSubnode(self.chatListEmptyNodeContainer)
self.chatListNode.isEmptyUpdated = { [weak self] isEmptyState, isFilter, transitionDirection, transition in self.addSubnode(self.debugListView)
filterBecameEmpty = { [weak self] _ in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
switch isEmptyState {
case .empty(false):
if case .group = strongSelf.groupId { if case .group = strongSelf.groupId {
strongSelf.dismissSelf?() 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)
} }
} }
filterEmptyAction = { [weak self] filter in
self.chatListEmptyNodeContainer.action = { [weak self] in guard let strongSelf = self else {
self?.emptyListAction?() return
}
strongSelf.emptyListAction?()
} }
self.addSubnode(self.debugListView)
} }
override func didLoad() { override func didLoad() {
@ -133,9 +685,8 @@ final class ChatListControllerNode: ASDisplayNode {
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor 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.searchDisplayController?.updatePresentationData(presentationData)
self.chatListEmptyNodeContainer.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings)
if let toolbarNode = self.toolbarNode { if let toolbarNode = self.toolbarNode {
toolbarNode.updateTheme(TabBarControllerTheme(rootControllerTheme: self.presentationData.theme)) 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) transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) self.containerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition)
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))
}
if let searchDisplayController = self.searchDisplayController { if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: transition) searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: transition)
@ -242,7 +778,7 @@ final class ChatListControllerNode: ASDisplayNode {
requestDeactivateSearch() requestDeactivateSearch()
} }
}) })
self.chatListNode.accessibilityElementsHidden = true self.containerNode.accessibilityElementsHidden = true
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: cleanNavigationBarHeight, transition: .immediate) self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
@ -260,23 +796,19 @@ final class ChatListControllerNode: ASDisplayNode {
if let searchDisplayController = self.searchDisplayController { if let searchDisplayController = self.searchDisplayController {
searchDisplayController.deactivate(placeholder: placeholderNode, animated: animated) searchDisplayController.deactivate(placeholder: placeholderNode, animated: animated)
self.searchDisplayController = nil self.searchDisplayController = nil
self.chatListNode.accessibilityElementsHidden = false self.containerNode.accessibilityElementsHidden = false
} }
} }
func playArchiveAnimation() { func playArchiveAnimation() {
self.chatListNode.forEachVisibleItemNode { node in self.containerNode.playArchiveAnimation()
if let node = node as? ChatListItemNode {
node.playArchiveAnimation()
}
}
} }
func scrollToTop() { func scrollToTop() {
if let searchDisplayController = self.searchDisplayController { if let searchDisplayController = self.searchDisplayController {
searchDisplayController.contentNode.scrollToTop() searchDisplayController.contentNode.scrollToTop()
} else { } else {
self.chatListNode.scrollToPosition(.top) self.containerNode.scrollToTop()
} }
} }
} }

View File

@ -18,13 +18,15 @@ private final class ChatListFilterPresetControllerArguments {
let openAddPeer: () -> Void let openAddPeer: () -> Void
let deleteAdditionalPeer: (PeerId) -> Void let deleteAdditionalPeer: (PeerId) -> Void
let setPeerIdWithRevealedOptions: (PeerId?, 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.context = context
self.updateState = updateState self.updateState = updateState
self.openAddPeer = openAddPeer self.openAddPeer = openAddPeer
self.deleteAdditionalPeer = deleteAdditionalPeer self.deleteAdditionalPeer = deleteAdditionalPeer
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.focusOnName = focusOnName
} }
} }
@ -168,13 +170,15 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case let .nameHeader(title): case let .nameHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .name(placeholder, value): 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 arguments.updateState { current in
var state = current var state = current
state.name = value state.name = value
return state return state
} }
}, action: {}) }, action: {}, cleared: {
arguments.focusOnName()
})
case let .typesHeader(text): case let .typesHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .filterPrivateChats(title, value): case let .filterPrivateChats(title, value):
@ -276,7 +280,7 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
} }
func chatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter) -> ViewController { 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 controller.navigationPresentation = .modal
let _ = (controller.result let _ = (controller.result
|> take(1) |> take(1)
@ -285,7 +289,7 @@ func chatListFilterAddChatsController(context: AccountContext, filter: ChatListF
var settings = settings var settings = settings
for i in 0 ..< settings.filters.count { for i in 0 ..< settings.filters.count {
if settings.filters[i].id == filter.id { if settings.filters[i].id == filter.id {
let previousIncludePeers = settings.filters[i].includePeers let previousIncludePeers = settings.filters[i].data.includePeers
var chatPeerIds: [PeerId] = [] var chatPeerIds: [PeerId] = []
for peerId in peerIds { for peerId in peerIds {
@ -296,7 +300,7 @@ func chatListFilterAddChatsController(context: AccountContext, filter: ChatListF
break break
} }
} }
settings.filters[i].includePeers = chatPeerIds + previousIncludePeers.filter { peerId in settings.filters[i].data.includePeers = chatPeerIds + previousIncludePeers.filter { peerId in
return !chatPeerIds.contains(peerId) return !chatPeerIds.contains(peerId)
} }
} }
@ -305,6 +309,8 @@ func chatListFilterAddChatsController(context: AccountContext, filter: ChatListF
}) })
|> deliverOnMainQueue).start(next: { settings in |> deliverOnMainQueue).start(next: { settings in
controller?.dismiss() controller?.dismiss()
let _ = replaceRemoteChatListFilters(account: context.account).start()
}) })
}) })
return controller return controller
@ -317,7 +323,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} else { } else {
initialName = "New Filter" 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 stateValue = Atomic(value: initialState)
let statePromise = ValuePromise(initialState, ignoreRepeated: true) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in
@ -331,6 +337,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
var presentControllerImpl: ((ViewController, Any?) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)?
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
var focusOnNameImpl: (() -> Void)?
let arguments = ChatListFilterPresetControllerArguments( let arguments = ChatListFilterPresetControllerArguments(
context: context, context: context,
@ -338,7 +345,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
updateState(f) updateState(f)
}, },
openAddPeer: { 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 addPeerDisposable.set((controller.result
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] peerIds in |> deliverOnMainQueue).start(next: { [weak controller] peerIds in
@ -377,6 +384,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} }
return state 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 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 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 let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in
var preset = preset var preset = preset
if currentPreset == nil { if currentPreset == nil {
@ -435,6 +445,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|> deliverOnMainQueue).start(next: { settings in |> deliverOnMainQueue).start(next: { settings in
updated(settings.filters) updated(settings.filters)
dismissImpl?() dismissImpl?()
let _ = replaceRemoteChatListFilters(account: context.account).start()
}) })
}) })
@ -455,6 +467,16 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
dismissImpl = { [weak controller] in dismissImpl = { [weak controller] in
let _ = controller?.dismiss() 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 return controller
} }

View File

@ -53,7 +53,7 @@ private enum ChatListFilterPresetListEntryStableId: Hashable {
private enum ChatListFilterPresetListEntry: ItemListNodeEntry { private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case listHeader(String) 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 addItem(text: String, isEditing: Bool)
case listFooter(String) case listFooter(String)
@ -99,8 +99,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
switch self { switch self {
case let .listHeader(text): case let .listHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .preset(index, title, preset, canBeReordered, canBeDeleted, isEditing): case let .preset(index, title, label, 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: { 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) arguments.openPreset(preset)
}, setItemWithRevealedOptions: { lhs, rhs in }, setItemWithRevealedOptions: { lhs, rhs in
arguments.setItemWithRevealedOptions(lhs, rhs) arguments.setItemWithRevealedOptions(lhs, rhs)
@ -122,14 +122,14 @@ private struct ChatListFilterPresetListControllerState: Equatable {
var revealedPreset: Int32? = nil 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] = [] var entries: [ChatListFilterPresetListEntry] = []
entries.append(.listHeader("FILTERS")) entries.append(.listHeader("FILTERS"))
for preset in filtersState.filters { for (filter, chatCount) in filters {
entries.append(.preset(index: entries.count, title: preset.title, preset: preset, canBeReordered: filtersState.filters.count > 1, canBeDeleted: true, isEditing: state.isEditing)) 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(.addItem(text: "Create New Filter", isEditing: state.isEditing))
} }
entries.append(.listFooter("Tap \"Edit\" to change the order or delete filters.")) entries.append(.listFooter("Tap \"Edit\" to change the order or delete filters."))
@ -137,7 +137,7 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
return entries 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 initialState = ChatListFilterPresetListControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState) let stateValue = Atomic(value: initialState)
@ -171,21 +171,51 @@ func chatListFilterPresetListController(context: AccountContext, updated: @escap
}) })
|> deliverOnMainQueue).start(next: { settings in |> deliverOnMainQueue).start(next: { settings in
updated(settings.filters) 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(), let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData, context.sharedContext.presentationData,
statePromise.get(), statePromise.get(),
filtersWithCounts,
preferences preferences
) )
|> map { presentationData, state, preferences -> (ItemListControllerState, (ItemListNodeState, Any)) in |> map { presentationData, state, filtersWithCounts, preferences -> (ItemListControllerState, (ItemListNodeState, Any)) in
let filtersState = preferences.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState ?? ChatListFiltersState.default
let filterSettings = preferences.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings] as? ChatListFilterSettings ?? ChatListFilterSettings.default 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 leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Close), style: .regular, enabled: true, action: {
let _ = replaceRemoteChatListFilters(account: context.account).start()
dismissImpl?() dismissImpl?()
}) })
let rightNavigationButton: ItemListNavigationButton 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 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)) return (controllerState, (listState, arguments))
} }
@ -217,6 +247,9 @@ func chatListFilterPresetListController(context: AccountContext, updated: @escap
let controller = ItemListController(context: context, state: signal) let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal controller.navigationPresentation = .modal
controller.willDisappear = { _ in
let _ = replaceRemoteChatListFilters(account: context.account).start()
}
pushControllerImpl = { [weak controller] c in pushControllerImpl = { [weak controller] c in
controller?.push(c) controller?.push(c)
} }

View File

@ -20,6 +20,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let preset: ChatListFilter let preset: ChatListFilter
let title: String let title: String
let label: String
let editing: ChatListFilterPresetListItemEditing let editing: ChatListFilterPresetListItemEditing
let canBeReordered: Bool let canBeReordered: Bool
let canBeDeleted: Bool let canBeDeleted: Bool
@ -32,6 +33,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
presentationData: ItemListPresentationData, presentationData: ItemListPresentationData,
preset: ChatListFilter, preset: ChatListFilter,
title: String, title: String,
label: String,
editing: ChatListFilterPresetListItemEditing, editing: ChatListFilterPresetListItemEditing,
canBeReordered: Bool, canBeReordered: Bool,
canBeDeleted: Bool, canBeDeleted: Bool,
@ -43,6 +45,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
self.presentationData = presentationData self.presentationData = presentationData
self.preset = preset self.preset = preset
self.title = title self.title = title
self.label = label
self.editing = editing self.editing = editing
self.canBeReordered = canBeReordered self.canBeReordered = canBeReordered
self.canBeDeleted = canBeDeleted self.canBeDeleted = canBeDeleted
@ -108,6 +111,8 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
private let maskNode: ASImageNode private let maskNode: ASImageNode
private let titleNode: TextNode private let titleNode: TextNode
private let labelNode: TextNode
private let arrowNode: ASImageNode
private let activateArea: AccessibilityAreaNode private let activateArea: AccessibilityAreaNode
@ -141,6 +146,14 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
self.titleNode.contentMode = .left self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale 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.activateArea = AccessibilityAreaNode()
self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode = ASDisplayNode()
@ -149,6 +162,8 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.activateArea) self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in 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) { func asyncLayout() -> (_ item: ChatListFilterPresetListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
@ -166,9 +182,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
return { item, params, neighbors in return { item, params, neighbors in
var updatedTheme: PresentationTheme? var updatedTheme: PresentationTheme?
var updateArrowImage: UIImage?
if currentItem?.presentationData.theme !== item.presentationData.theme { if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme updatedTheme = item.presentationData.theme
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
} }
let peerRevealOptions: [ItemListRevealOption] let peerRevealOptions: [ItemListRevealOption]
@ -187,21 +205,27 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
var editingOffset: CGFloat = 0.0 var editingOffset: CGFloat = 0.0
var reorderInset: 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) let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
editableControlSizeAndApply = sizeAndApply editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0 editingOffset = sizeAndApply.0
if item.canBeReordered {
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme) let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = reorderSizeAndApply reorderControlSizeAndApply = reorderSizeAndApply
reorderInset = reorderSizeAndApply.0 reorderInset = reorderSizeAndApply.0
} }
}
let leftInset: CGFloat = 16.0 + params.leftInset let leftInset: CGFloat = 16.0 + params.leftInset
let rightInset: CGFloat = params.rightInset + max(reorderInset, 55.0) 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 (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 insets = itemListNeighborsGroupedInsets(neighbors)
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + 11.0 * 2.0) let contentSize = CGSize(width: params.width, height: titleLayout.size.height + 11.0 * 2.0)
let separatorHeight = UIScreenPixel let separatorHeight = UIScreenPixel
@ -280,6 +304,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
} }
let _ = titleApply() let _ = titleApply()
let _ = labelApply()
if strongSelf.backgroundNode.supernode == nil { if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) 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)) 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.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)) 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) { private var previousSelectedAbsFrame: CGRect?
let focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter private var previousSelectedFrame: CGRect?
var previousSelectedAbsFrame: 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 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 { if self.currentParams?.presentationData.theme !== presentationData.theme {
self.selectedLineNode.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in 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 let sideInset: CGFloat = 16.0
var leftOffset: CGFloat = sideInset 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) 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? var selectedFrame: CGRect?
if let selectedFilter = selectedFilter, let currentIndex = filters.index(where: { $0.id == selectedFilter }) { if let selectedFilter = selectedFilter, let currentIndex = filters.index(where: { $0.id == selectedFilter }) {
func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
@ -422,28 +422,41 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
} else { } else {
transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) 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 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)) 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)) 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 { } 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))) 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)) 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 let contentOffsetX: CGFloat
if previousScrollBounds.minX.isZero { if previousScrollBounds.minX.isZero {
contentOffsetX = 0.0 contentOffsetX = 0.0
} else if previousScrollBounds.maxX == previousScrollBounds.width { } else if previousScrollBounds.maxX == previousScrollBounds.width {
contentOffsetX = self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width contentOffsetX = self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width
} else { } 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)) 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 { } else {
self.selectedLineNode.isHidden = true self.selectedLineNode.isHidden = true
self.previousSelectedAbsFrame = nil
self.previousSelectedFrame = nil
} }
} }
} }

View File

@ -17,7 +17,7 @@ import SearchUI
public enum ChatListNodeMode { public enum ChatListNodeMode {
case chatList case chatList
case peers(filter: ChatListNodePeersFilter) case peers(filter: ChatListNodePeersFilter, isSelecting: Bool)
} }
struct ChatListNodeListViewTransition { struct ChatListNodeListViewTransition {
@ -153,7 +153,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
switch mode { switch mode {
case .chatList: 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) 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 let itemPeer = peer.chatMainPeer
var chatPeer: Peer? var chatPeer: Peer?
if let peer = peer.peers[peer.peerId] { 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 { if let chatPeer = chatPeer {
nodeInteraction.peerSelected(chatPeer) nodeInteraction.peerSelected(chatPeer)
} }
@ -245,7 +245,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
switch mode { switch mode {
case .chatList: 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) 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 let itemPeer = peer.chatMainPeer
var chatPeer: Peer? var chatPeer: Peer?
if let peer = peer.peers[peer.peerId] { if let peer = peer.peers[peer.peerId] {
@ -266,7 +266,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
enabled = false 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 { if let chatPeer = chatPeer {
nodeInteraction.peerSelected(chatPeer) nodeInteraction.peerSelected(chatPeer)
} }
@ -352,7 +352,7 @@ public final class ChatListNode: ListView {
return _contentsReady.get() return _contentsReady.get()
} }
public var peerSelected: ((PeerId, Bool, Bool) -> Void)? public var peerSelected: ((Peer, Bool, Bool) -> Void)?
public var disabledPeerSelected: ((Peer) -> Void)? public var disabledPeerSelected: ((Peer) -> Void)?
public var groupSelected: ((PeerGroupId) -> Void)? public var groupSelected: ((PeerGroupId) -> Void)?
public var addContact: ((String) -> Void)? public var addContact: ((String) -> Void)?
@ -373,7 +373,7 @@ public final class ChatListNode: ListView {
private var dequeuedInitialTransitionOnLayout = false private var dequeuedInitialTransitionOnLayout = false
private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)? private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)?
private(set) var currentState: ChatListNodeState public private(set) var currentState: ChatListNodeState
private let statePromise: ValuePromise<ChatListNodeState> private let statePromise: ValuePromise<ChatListNodeState>
public var state: Signal<ChatListNodeState, NoError> { public var state: Signal<ChatListNodeState, NoError> {
return self.statePromise.get() return self.statePromise.get()
@ -453,7 +453,12 @@ public final class ChatListNode: ListView {
self.controlsHistoryPreload = controlsHistoryPreload self.controlsHistoryPreload = controlsHistoryPreload
self.mode = mode 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.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
self.theme = theme self.theme = theme
@ -471,7 +476,7 @@ public final class ChatListNode: ListView {
} }
}, peerSelected: { [weak self] peer in }, peerSelected: { [weak self] peer in
if let strongSelf = self, let peerSelected = strongSelf.peerSelected { if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
peerSelected(peer.id, true, false) peerSelected(peer, true, false)
} }
}, disabledPeerSelected: { [weak self] peer in }, disabledPeerSelected: { [weak self] peer in
if let strongSelf = self, let disabledPeerSelected = strongSelf.disabledPeerSelected { if let strongSelf = self, let disabledPeerSelected = strongSelf.disabledPeerSelected {
@ -491,7 +496,7 @@ public final class ChatListNode: ListView {
} }
}, messageSelected: { [weak self] peer, message, isAd in }, messageSelected: { [weak self] peer, message, isAd in
if let strongSelf = self, let peerSelected = strongSelf.peerSelected { if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
peerSelected(peer.id, true, isAd) peerSelected(peer, true, isAd)
} }
}, groupSelected: { [weak self] groupId in }, groupSelected: { [weak self] groupId in
if let strongSelf = self, let groupSelected = strongSelf.groupSelected { if let strongSelf = self, let groupSelected = strongSelf.groupSelected {
@ -586,7 +591,7 @@ public final class ChatListNode: ListView {
let currentRemovingPeerId = self.currentRemovingPeerId let currentRemovingPeerId = self.currentRemovingPeerId
let savedMessagesPeer: Signal<Peer?, NoError> 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) savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map(Optional.init) |> map(Optional.init)
} else { } else {
@ -639,7 +644,7 @@ public final class ChatListNode: ListView {
switch mode { switch mode {
case .chatList: case .chatList:
return true return true
case let .peers(filter): case let .peers(filter, _):
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false } 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(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser 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) { 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 }) 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 let entryCount = chatListView.filteredEntries.count
var current: (ChatListIndex, PeerId, Int)? = nil var current: (ChatListIndex, Peer, Int)? = nil
var previous: (ChatListIndex, PeerId)? = nil var previous: (ChatListIndex, Peer)? = nil
var next: (ChatListIndex, PeerId)? = nil var next: (ChatListIndex, Peer)? = nil
outer: for i in range.firstIndex ..< range.lastIndex { outer: for i in range.firstIndex ..< range.lastIndex {
if i < 0 || i >= entryCount { if i < 0 || i >= entryCount {
@ -1536,7 +1570,7 @@ public final class ChatListNode: ListView {
switch chatListView.filteredEntries[entryCount - i - 1] { switch chatListView.filteredEntries[entryCount - i - 1] {
case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _): case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _):
if interaction.highlightedChatLocation?.location == ChatLocation.peer(peer.peerId) { if interaction.highlightedChatLocation?.location == ChatLocation.peer(peer.peerId) {
current = (index, peer.peerId, entryCount - i - 1) current = (index, peer.peer!, entryCount - i - 1)
break outer break outer
} }
default: default:
@ -1556,22 +1590,35 @@ public final class ChatListNode: ListView {
} else { } else {
position = .later(than: nil) position = .later(than: nil)
} }
let _ = (relativeUnreadChatListIndex(position: position) |> deliverOnMainQueue).start(next: { [weak self] index in let postbox = self.context.account.postbox
guard let strongSelf = self, let index = index else { 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 return
} }
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: strongSelf.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound, scrollPosition: .center(.top), animated: true, filter: strongSelf.chatListFilter) let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: strongSelf.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound, scrollPosition: .center(.top), animated: true, filter: strongSelf.chatListFilter)
strongSelf.setChatListLocation(location) strongSelf.setChatListLocation(location)
strongSelf.peerSelected?(index.messageIndex.id.peerId, false, false) strongSelf.peerSelected?(peer, false, false)
}) })
case .previous(unread: false), .next(unread: 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 let current = current, entryCount > 1 {
if current.2 > 0, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 - 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] { 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 { if case .previous = option {
target = previous target = previous
@ -1580,7 +1627,7 @@ public final class ChatListNode: ListView {
} }
} else if entryCount > 0 { } else if entryCount > 0 {
if case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[entryCount - 1] { if case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[entryCount - 1] {
target = (index, peer.peerId) target = (index, peer.peer!)
} }
} }
if let target = target { if let target = target {
@ -1589,7 +1636,15 @@ public final class ChatListNode: ListView {
self.peerSelected?(target.1, false, false) self.peerSelected?(target.1, false, false)
} }
case let .peerId(peerId): 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): case let .index(index):
guard index < 10 else { guard index < 10 else {
return return
@ -1607,7 +1662,7 @@ public final class ChatListNode: ListView {
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _) = entries[10 - index - 1] { 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) let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter)
self.setChatListLocation(location) 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? let scrollPosition: ChatListNodeViewScrollPosition?
} }
func chatListFilterPredicate(filter: ChatListFilter) -> ChatListFilterPredicate { func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredicate {
let includePeers = Set(filter.includePeers) let includePeers = Set(filter.includePeers)
return ChatListFilterPredicate(includePeerIds: includePeers, include: { peer, notificationSettings, isUnread in return ChatListFilterPredicate(includePeerIds: includePeers, include: { peer, notificationSettings, isUnread in
if filter.excludeRead { if filter.excludeRead {
@ -97,7 +97,7 @@ func chatListFilterPredicate(filter: ChatListFilter) -> ChatListFilterPredicate
} }
func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> { 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 { switch location {
case let .initial(count, _): case let .initial(count, _):

View File

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

View File

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

View File

@ -111,7 +111,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
override func didLoad() { override func didLoad() {
super.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 { guard let strongSelf = self, strongSelf.controllers.count > 1 else {
return [] return []
} }

View File

@ -90,7 +90,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
self.scrollNode.view.clipsToBounds = false self.scrollNode.view.clipsToBounds = false
self.scrollNode.view.delegate = self 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 { guard let strongSelf = self, !strongSelf.isDismissed else {
return [] 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.theme = theme
self.navigationBar = navigationBar 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 self.toolbarActionSelected = toolbarActionSelected
super.init() super.init()

View File

@ -240,6 +240,13 @@ open class TabBarController: ViewController {
if index >= 0 && index < strongSelf.controllers.count { if index >= 0 && index < strongSelf.controllers.count {
strongSelf.controllers[index].tabBarItemContextAction(sourceNode: node, gesture: gesture) 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 }, toolbarActionSelected: { [weak self] action in
self?.currentController?.toolbarActionSelected(action: action) 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) private let badgeFont = Font.regular(13.0)
public enum TabBarItemSwipeDirection {
case left
case right
}
private final class TabBarItemNode: ASDisplayNode { private final class TabBarItemNode: ASDisplayNode {
let extractedContainerNode: ContextExtractedContentContainingNode let extractedContainerNode: ContextExtractedContentContainingNode
let containerNode: ContextControllerSourceNode let containerNode: ContextControllerSourceNode
@ -97,6 +102,8 @@ private final class TabBarItemNode: ASDisplayNode {
let contextTextImageNode: ASImageNode let contextTextImageNode: ASImageNode
var contentWidth: CGFloat? var contentWidth: CGFloat?
var swiped: ((TabBarItemSwipeDirection) -> Void)?
override init() { override init() {
self.extractedContainerNode = ContextExtractedContentContainingNode() self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode() 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.contextImageNode, alpha: isExtracted ? 1.0 : 0.0)
transition.updateAlpha(node: strongSelf.contextTextImageNode, 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 selectedImageValue: UIImage?
var appliedSelectedImageValue: 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.item = item.item
self.imageNode = imageNode self.imageNode = imageNode
@ -225,6 +252,9 @@ private final class TabBarNodeContainer {
} }
contextAction(strongSelf.imageNode.extractedContainerNode, gesture) contextAction(strongSelf.imageNode.extractedContainerNode, gesture)
} }
imageNode.swiped = { [weak self] direction in
swipeAction(direction)
}
imageNode.containerNode.isGestureEnabled = item.hasContext imageNode.containerNode.isGestureEnabled = item.hasContext
} }
@ -269,6 +299,7 @@ class TabBarNode: ASDisplayNode {
private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void
private let contextAction: (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void private let contextAction: (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void
private let swipeAction: (Int, TabBarItemSwipeDirection) -> Void
private var theme: TabBarControllerTheme private var theme: TabBarControllerTheme
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)?
@ -282,9 +313,10 @@ class TabBarNode: ASDisplayNode {
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? 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.itemSelected = itemSelected
self.contextAction = contextAction self.contextAction = contextAction
self.swipeAction = swipeAction
self.theme = theme self.theme = theme
self.separatorNode = ASDisplayNode() self.separatorNode = ASDisplayNode()
@ -381,6 +413,8 @@ class TabBarNode: ASDisplayNode {
}, contextAction: { [weak self] node, gesture in }, contextAction: { [weak self] node, gesture in
self?.tapRecognizer?.cancel() self?.tapRecognizer?.cancel()
self?.contextAction(i, node, gesture) self?.contextAction(i, node, gesture)
}, swipeAction: { [weak self] direction in
self?.swipeAction(i, direction)
}) })
if let selectedIndex = self.selectedIndex, selectedIndex == i { 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) 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 tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
} }
open func tabBarItemSwipeAction(direction: TabBarItemSwipeDirection) {
}
} }
func traceIsOpaque(layer: CALayer, rect: CGRect) -> Bool { 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(_:))) let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:)))
self.recognizer = recognizer self.recognizer = recognizer
recognizer.delegate = self
recognizer.allowAnyDirection = self.allowAnyDirection recognizer.allowAnyDirection = self.allowAnyDirection
self.view.addGestureRecognizer(recognizer) self.view.addGestureRecognizer(recognizer)

View File

@ -45,9 +45,10 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let shouldUpdateText: (String) -> Bool let shouldUpdateText: (String) -> Bool
let processPaste: ((String) -> String)? let processPaste: ((String) -> String)?
let updatedFocus: ((Bool) -> Void)? let updatedFocus: ((Bool) -> Void)?
let cleared: (() -> Void)?
public let tag: ItemListItemTag? 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.presentationData = presentationData
self.title = title self.title = title
self.text = text self.text = text
@ -64,6 +65,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
self.processPaste = processPaste self.processPaste = processPaste
self.updatedFocus = updatedFocus self.updatedFocus = updatedFocus
self.action = action 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) { 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() { @objc private func clearButtonPressed() {
self.textNode.textField.text = "" self.textNode.textField.text = ""
self.textUpdated("") self.textUpdated("")
self.item?.cleared?()
} }
private func textUpdated(_ text: String) { private func textUpdated(_ text: String) {

View File

@ -641,6 +641,25 @@ final class ChatListTable: Table {
return entries 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? { func getStandalone(peerId: PeerId, messageHistoryTable: MessageHistoryTable) -> ChatListIntermediateEntry? {
let index = self.indexTable.get(peerId: peerId) let index = self.indexTable.get(peerId: peerId)
switch index.inclusion { switch index.inclusion {

View File

@ -320,6 +320,7 @@ final class MutableChatListView {
fileprivate var additionalItemIds: Set<PeerId> fileprivate var additionalItemIds: Set<PeerId>
fileprivate var additionalItemEntries: [MutableChatListEntry] fileprivate var additionalItemEntries: [MutableChatListEntry]
fileprivate var additionalMixedItemIds: Set<PeerId> fileprivate var additionalMixedItemIds: Set<PeerId>
fileprivate var additionalMixedPinnedItemIds: Set<PeerId>
fileprivate var additionalMixedItemEntries: [MutableChatListEntry] fileprivate var additionalMixedItemEntries: [MutableChatListEntry]
fileprivate var earlier: MutableChatListEntry? fileprivate var earlier: MutableChatListEntry?
fileprivate var later: MutableChatListEntry? fileprivate var later: MutableChatListEntry?
@ -340,10 +341,17 @@ final class MutableChatListView {
self.additionalItemEntries = [] self.additionalItemEntries = []
self.additionalMixedItemEntries = [] self.additionalMixedItemEntries = []
self.additionalMixedItemIds = Set() self.additionalMixedItemIds = Set()
self.additionalMixedPinnedItemIds = Set()
if let filterPredicate = self.filterPredicate { if let filterPredicate = self.filterPredicate {
self.additionalMixedItemIds.formUnion(filterPredicate.includePeerIds) 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) { 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)) 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 !updatedPeerNotificationSettings.isEmpty {
if let filterPredicate = self.filterPredicate { if let filterPredicate = self.filterPredicate {
for (peerId, settingsChange) in updatedPeerNotificationSettings { 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 isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let wasIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.0, isUnread: isUnread) let wasIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.0, isUnread: isUnread)
let isIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.1, isUnread: isUnread) let isIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.1, isUnread: isUnread)
@ -611,6 +619,22 @@ final class MutableChatListView {
continue 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 { if !transaction.updatedFailedMessagePeerIds.isEmpty {
@ -723,7 +747,7 @@ final class MutableChatListView {
hasChanges = true hasChanges = true
} }
var updateAdditionalMixedItems = false var updateAdditionalMixedItems = false
for peerId in self.additionalMixedItemIds { for peerId in self.additionalMixedItemIds.union(self.additionalMixedPinnedItemIds) {
if transaction.currentOperationsByPeerId[peerId] != nil { if transaction.currentOperationsByPeerId[peerId] != nil {
updateAdditionalMixedItems = true updateAdditionalMixedItems = true
} }
@ -736,7 +760,7 @@ final class MutableChatListView {
} }
if updateAdditionalMixedItems { if updateAdditionalMixedItems {
self.additionalMixedItemEntries.removeAll() 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) { 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)) 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) { if !filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread: isUnread) {
return false return false
} }
if self.additionalMixedItemIds.contains(peer.id) {
return false
}
if self.additionalMixedPinnedItemIds.contains(peer.id) {
return false
}
} else { } else {
return false return false
} }
@ -1076,12 +1106,17 @@ public final class ChatListView {
existingIds.insert(messageEntry.0.messageIndex.id.peerId) 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 case let .MessageEntry(messageEntry) = entry {
if !existingIds.contains(messageEntry.0.messageIndex.id.peerId) { if !existingIds.contains(messageEntry.0.messageIndex.id.peerId) {
switch entry { switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed): 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): case let .HoleEntry(hole):
entries.append(.HoleEntry(hole)) entries.append(.HoleEntry(hole))
case .IntermediateMessageEntry: case .IntermediateMessageEntry:
@ -1104,12 +1139,8 @@ public final class ChatListView {
additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed)) additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed))
case .HoleEntry: case .HoleEntry:
assertionFailure() assertionFailure()
/*case .GroupReferenceEntry:
assertionFailure()*/
case .IntermediateMessageEntry: case .IntermediateMessageEntry:
assertionFailure() 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 { public func legacyGetAccessChallengeData() -> PostboxAccessChallengeData {
assert(!self.disposed) assert(!self.disposed)
if let postbox = self.postbox { if let postbox = self.postbox {
@ -1657,9 +1690,13 @@ public final class Postbox {
return { entry in return { entry in
switch entry { switch entry {
case let .message(index, _, _): case let .message(index, _, _):
if index.pinningIndex != nil {
return false
}
if let peer = self.peerTable.get(index.messageIndex.id.peerId) { if let peer = self.peerTable.get(index.messageIndex.id.peerId) {
let isUnread = self.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false 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 return true
} else { } else {
return false 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) { public func filteredRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> ValueBoxFilterResult, limit: Int) {
var currentStart = start var currentStart = start
var acceptedCount = 0 var acceptedCount = 0
while acceptedCount < limit { while true {
if limit > 0 && acceptedCount >= limit {
break
}
var hadStop = false var hadStop = false
var lastKey: ValueBoxKey? var lastKey: ValueBoxKey?
self.range(table, start: currentStart, end: end, values: { key, value in 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) { public func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> Bool, limit: Int) {
precondition(self.queue.isCurrent()) precondition(self.queue.isCurrent())
if let _ = self.tables[table.id] { 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 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, 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 range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> Bool, limit: Int)
func scan(_ table: ValueBoxTable, values: (ValueBoxKey, ReadBuffer) -> Bool) func scan(_ table: ValueBoxTable, values: (ValueBoxKey, ReadBuffer) -> Bool)
func scan(_ table: ValueBoxTable, keys: (ValueBoxKey) -> Bool) func scan(_ table: ValueBoxTable, keys: (ValueBoxKey) -> Bool)

View File

@ -114,6 +114,7 @@ private final class SettingsItemArguments {
let openPhoneNumberChange: () -> Void let openPhoneNumberChange: () -> Void
let accountContextAction: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void let accountContextAction: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void
let openDevices: () -> Void let openDevices: () -> Void
let openFilters: () -> Void
init( init(
sharedContext: SharedAccountContext, sharedContext: SharedAccountContext,
@ -147,7 +148,8 @@ private final class SettingsItemArguments {
keepPhone: @escaping () -> Void, keepPhone: @escaping () -> Void,
openPhoneNumberChange: @escaping () -> Void, openPhoneNumberChange: @escaping () -> Void,
accountContextAction: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void, accountContextAction: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void,
openDevices: @escaping () -> Void openDevices: @escaping () -> Void,
openFilters: @escaping () -> Void
) { ) {
self.sharedContext = sharedContext self.sharedContext = sharedContext
self.avatarAndNameInfoContext = avatarAndNameInfoContext self.avatarAndNameInfoContext = avatarAndNameInfoContext
@ -181,6 +183,7 @@ private final class SettingsItemArguments {
self.openPhoneNumberChange = openPhoneNumberChange self.openPhoneNumberChange = openPhoneNumberChange
self.accountContextAction = accountContextAction self.accountContextAction = accountContextAction
self.openDevices = openDevices self.openDevices = openDevices
self.openFilters = openFilters
} }
} }
@ -211,6 +214,8 @@ private indirect enum SettingsEntry: ItemListNodeEntry {
case devices(PresentationTheme, UIImage?, String, String) case devices(PresentationTheme, UIImage?, String, String)
case filters(PresentationTheme, UIImage?, String, String)
case savedMessages(PresentationTheme, UIImage?, String) case savedMessages(PresentationTheme, UIImage?, String)
case recentCalls(PresentationTheme, UIImage?, String) case recentCalls(PresentationTheme, UIImage?, String)
case stickers(PresentationTheme, UIImage?, String, String, [ArchivedStickerPackItem]?) case stickers(PresentationTheme, UIImage?, String, String, [ArchivedStickerPackItem]?)
@ -240,7 +245,7 @@ private indirect enum SettingsEntry: ItemListNodeEntry {
return SettingsSection.accounts.rawValue return SettingsSection.accounts.rawValue
case .proxy: case .proxy:
return SettingsSection.proxy.rawValue return SettingsSection.proxy.rawValue
case .devices: case .devices, .filters:
return SettingsSection.media.rawValue return SettingsSection.media.rawValue
case .savedMessages, .recentCalls, .stickers: case .savedMessages, .recentCalls, .stickers:
return SettingsSection.media.rawValue return SettingsSection.media.rawValue
@ -285,30 +290,32 @@ private indirect enum SettingsEntry: ItemListNodeEntry {
return 1006 return 1006
case .devices: case .devices:
return 1007 return 1007
case .notificationsAndSounds: case .filters:
return 1008 return 1008
case .privacyAndSecurity: case .notificationsAndSounds:
return 1009 return 1009
case .dataAndStorage: case .privacyAndSecurity:
return 1010 return 1010
case .themes: case .dataAndStorage:
return 1011 return 1011
case .language: case .themes:
return 1012 return 1012
case .contentStickers: case .language:
return 1013 return 1013
case .contentStickers:
return 1014
#if ENABLE_WALLET #if ENABLE_WALLET
case .wallet: case .wallet:
return 1014 return 1015
#endif #endif
case .passport: case .passport:
return 1015
case .watch:
return 1016 return 1016
case .askAQuestion: case .watch:
return 1017 return 1017
case .faq: case .askAQuestion:
return 1018 return 1018
case .faq:
return 1019
} }
} }
@ -406,6 +413,12 @@ private indirect enum SettingsEntry: ItemListNodeEntry {
} else { } else {
return false 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): case let .savedMessages(lhsTheme, lhsImage, lhsText):
if case let .savedMessages(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { if case let .savedMessages(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText {
return true 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: { return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.openDevices() 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): case let .savedMessages(theme, image, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.openSavedMessages() arguments.openSavedMessages()
@ -635,7 +652,7 @@ private struct SettingsState: Equatable {
var isSearching: Bool 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] = [] var entries: [SettingsEntry] = []
if let peer = peerViewMainPeer(view) as? TelegramUser { if let peer = peerViewMainPeer(view) as? TelegramUser {
@ -688,6 +705,10 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
} else { } else {
entries.append(.devices(presentationData.theme, UIImage(bundleImageName: "Settings/MenuIcons/Sessions")?.precomposed(), presentationData.strings.Settings_Devices, otherSessionCount == 0 ? "" : "\(otherSessionCount + 1)")) 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) let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed)
entries.append(.notificationsAndSounds(presentationData.theme, PresentationResourcesSettings.notifications, presentationData.strings.Settings_NotificationsAndSounds, notifyExceptions, notificationsWarning)) 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 privacySettings = Promise<AccountPrivacySettings?>(nil)
let enableQRLogin = Promise<Bool>() let enableQRLogin = Promise<Bool>()
let enableFilters = Promise<Bool>()
let openFaq: (Promise<ResolvedUrl>, String?) -> Void = { resolvedUrl, customAnchor in let openFaq: (Promise<ResolvedUrl>, String?) -> Void = { resolvedUrl, customAnchor in
let _ = (contextValue.get() let _ = (contextValue.get()
@ -1221,6 +1243,9 @@ public func settingsController(context: AccountContext, accountManager: AccountM
gesture?.cancel() gesture?.cancel()
} }
}, openDevices: { }, openDevices: {
let _ = (contextValue.get()
|> deliverOnMainQueue
|> take(1)).start(next: { context in
let _ = (combineLatest(queue: .mainQueue(), let _ = (combineLatest(queue: .mainQueue(),
activeSessionsContextAndCount.get(), activeSessionsContextAndCount.get(),
enableQRLogin.get() enableQRLogin.get()
@ -1234,6 +1259,13 @@ public func settingsController(context: AccountContext, accountManager: AccountM
} }
}) })
}) })
}, openFilters: {
let _ = (contextValue.get()
|> deliverOnMainQueue
|> take(1)).start(next: { context in
pushControllerImpl?(chatListFilterPresetListController(context: context, updated: { _ in }))
})
})
changeProfilePhotoImpl = { changeProfilePhotoImpl = {
let _ = (contextValue.get() let _ = (contextValue.get()
@ -1496,7 +1528,21 @@ public func settingsController(context: AccountContext, accountManager: AccountM
} }
enableQRLogin.set(enableQRLoginSignal) 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 |> map { context, presentationData, state, view, preferencesAndExceptions, featuredAndArchived, hasWalletPassportAndWatch, accountsAndPeers, activeSessionsContextAndCount -> (ItemListControllerState, (ItemListNodeState, Any)) in
let otherSessionCount = activeSessionsContextAndCount.1 let otherSessionCount = activeSessionsContextAndCount.1
@ -1536,8 +1582,8 @@ public func settingsController(context: AccountContext, accountManager: AccountM
pushControllerImpl?(c) 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 })) }, 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 (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), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)) 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)) 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(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(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(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>] = [ let importantBackgroundOperations: [Signal<AccountRunningImportantTasks, NoError>] = [
managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] }, managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] },

View File

@ -5,7 +5,19 @@ import TelegramApi
import SyncCore 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 var rawValue: Int32
public init(rawValue: Int32) { public init(rawValue: Int32) {
@ -93,50 +105,58 @@ extension ChatListFilterPeerCategories {
} }
} }
public struct ChatListFilter: PostboxCoding, Equatable { public struct ChatListFilterData: Equatable, Hashable {
public var id: Int32
public var title: String?
public var categories: ChatListFilterPeerCategories public var categories: ChatListFilterPeerCategories
public var excludeMuted: Bool public var excludeMuted: Bool
public var excludeRead: Bool public var excludeRead: Bool
public var includePeers: [PeerId] public var includePeers: [PeerId]
public init( public init(
id: Int32,
title: String?,
categories: ChatListFilterPeerCategories, categories: ChatListFilterPeerCategories,
excludeMuted: Bool, excludeMuted: Bool,
excludeRead: Bool, excludeRead: Bool,
includePeers: [PeerId] includePeers: [PeerId]
) { ) {
self.id = id
self.title = title
self.categories = categories self.categories = categories
self.excludeMuted = excludeMuted self.excludeMuted = excludeMuted
self.excludeRead = excludeRead self.excludeRead = excludeRead
self.includePeers = includePeers 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) { public init(decoder: PostboxDecoder) {
self.id = decoder.decodeInt32ForKey("id", orElse: 0) self.id = decoder.decodeInt32ForKey("id", orElse: 0)
self.title = decoder.decodeOptionalStringForKey("title") self.title = decoder.decodeStringForKey("title", orElse: "")
self.categories = ChatListFilterPeerCategories(rawValue: decoder.decodeInt32ForKey("categories", orElse: 0)) self.data = ChatListFilterData(
self.excludeMuted = decoder.decodeInt32ForKey("excludeMuted", orElse: 0) != 0 categories: ChatListFilterPeerCategories(rawValue: decoder.decodeInt32ForKey("categories", orElse: 0)),
self.excludeRead = decoder.decodeInt32ForKey("excludeRead", orElse: 0) != 0 excludeMuted: decoder.decodeInt32ForKey("excludeMuted", orElse: 0) != 0,
self.includePeers = decoder.decodeInt64ArrayForKey("includePeers").map(PeerId.init) excludeRead: decoder.decodeInt32ForKey("excludeRead", orElse: 0) != 0,
includePeers: decoder.decodeInt64ArrayForKey("includePeers").map(PeerId.init)
)
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.id, forKey: "id") encoder.encodeInt32(self.id, forKey: "id")
if let title = self.title { encoder.encodeString(self.title, forKey: "title")
encoder.encodeString(title, forKey: "title") encoder.encodeInt32(self.data.categories.rawValue, forKey: "categories")
} else { encoder.encodeInt32(self.data.excludeMuted ? 1 : 0, forKey: "excludeMuted")
encoder.encodeNil(forKey: "title") encoder.encodeInt32(self.data.excludeRead ? 1 : 0, forKey: "excludeRead")
} encoder.encodeInt64Array(self.data.includePeers.map { $0.toInt64() }, forKey: "includePeers")
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")
} }
} }
@ -146,7 +166,8 @@ extension ChatListFilter {
case let .dialogFilter(flags, id, title, includePeers): case let .dialogFilter(flags, id, title, includePeers):
self.init( self.init(
id: id, id: id,
title: title.isEmpty ? nil : title, title: title,
data: ChatListFilterData(
categories: ChatListFilterPeerCategories(apiFlags: flags), categories: ChatListFilterPeerCategories(apiFlags: flags),
excludeMuted: (flags & (1 << 11)) != 0, excludeMuted: (flags & (1 << 11)) != 0,
excludeRead: (flags & (1 << 12)) != 0, excludeRead: (flags & (1 << 12)) != 0,
@ -163,19 +184,20 @@ extension ChatListFilter {
} }
} }
) )
)
} }
} }
func apiFilter(transaction: Transaction) -> Api.DialogFilter { func apiFilter(transaction: Transaction) -> Api.DialogFilter {
var flags: Int32 = 0 var flags: Int32 = 0
if self.excludeMuted { if self.data.excludeMuted {
flags |= 1 << 11 flags |= 1 << 11
} }
if self.excludeRead { if self.data.excludeRead {
flags |= 1 << 12 flags |= 1 << 12
} }
flags |= self.categories.apiFlags flags |= self.data.categories.apiFlags
return .dialogFilter(flags: flags, id: self.id, title: self.title ?? "", includePeers: self.includePeers.compactMap { peerId -> Api.InputPeer? in 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) return transaction.getPeer(peerId).flatMap(apiInputPeer)
}) })
} }
@ -287,6 +309,11 @@ func managedChatListFilters(postbox: Postbox, network: Network) -> Signal<Never,
} }
public func replaceRemoteChatListFilters(account: Account) -> Signal<Never, NoError> { public func replaceRemoteChatListFilters(account: Account) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> [ChatListFilter] in
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default
return settings.filters
}
|> mapToSignal { filters -> Signal<Never, NoError> in
return requestChatListFilters(postbox: account.postbox, network: account.network) return requestChatListFilters(postbox: account.postbox, network: account.network)
|> `catch` { _ -> Signal<[ChatListFilter], NoError> in |> `catch` { _ -> Signal<[ChatListFilter], NoError> in
return .complete() return .complete()
@ -294,12 +321,14 @@ public func replaceRemoteChatListFilters(account: Account) -> Signal<Never, NoEr
|> mapToSignal { remoteFilters -> Signal<Never, NoError> in |> mapToSignal { remoteFilters -> Signal<Never, NoError> in
var deleteSignals: [Signal<Never, NoError>] = [] var deleteSignals: [Signal<Never, NoError>] = []
for filter in remoteFilters { for filter in remoteFilters {
if !filters.contains(where: { $0.id == filter.id }) {
deleteSignals.append(requestUpdateChatListFilter(account: account, id: filter.id, filter: nil) deleteSignals.append(requestUpdateChatListFilter(account: account, id: filter.id, filter: nil)
|> `catch` { _ -> Signal<Never, NoError> in |> `catch` { _ -> Signal<Never, NoError> in
return .complete() return .complete()
} }
|> ignoreValues) |> ignoreValues)
} }
}
let addFilters = account.postbox.transaction { transaction -> [(Int32, ChatListFilter)] in let addFilters = account.postbox.transaction { transaction -> [(Int32, ChatListFilter)] in
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default
@ -310,16 +339,29 @@ public func replaceRemoteChatListFilters(account: Account) -> Signal<Never, NoEr
|> mapToSignal { filters -> Signal<Never, NoError> in |> mapToSignal { filters -> Signal<Never, NoError> in
var signals: [Signal<Never, NoError>] = [] var signals: [Signal<Never, NoError>] = []
for (id, filter) in filters { for (id, filter) in filters {
if !remoteFilters.contains(filter) {
signals.append(requestUpdateChatListFilter(account: account, id: id, filter: filter) signals.append(requestUpdateChatListFilter(account: account, id: id, filter: filter)
|> `catch` { _ -> Signal<Never, NoError> in |> `catch` { _ -> Signal<Never, NoError> in
return .complete() return .complete()
} }
|> ignoreValues) |> ignoreValues)
} }
}
return combineLatest(signals) return combineLatest(signals)
|> ignoreValues |> 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()
}
} else {
reorderFilters = .complete()
}
return combineLatest( return combineLatest(
deleteSignals deleteSignals
) )
@ -327,6 +369,10 @@ public func replaceRemoteChatListFilters(account: Account) -> Signal<Never, NoEr
|> then( |> then(
addFilters addFilters
) )
|> then(
reorderFilters
)
}
} }
} }

View File

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

View File

@ -83,7 +83,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
self.scrollToTop = { [weak self] in self.scrollToTop = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.contactsNode.contactListNode.scrollToTop() strongSelf.contactsNode.scrollToTop()
} }
} }
@ -134,7 +134,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
switch self.mode { switch self.mode {
case .groupCreation: case .groupCreation:
let maxCount: Int32 = self.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 let maxCount: Int32 = self.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
let count = self.contactsNode.contactListNode.selectionState?.selectedPeerIndices.count ?? 0 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)") 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)) let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton self.rightNavigationButton = rightNavigationButton
@ -153,12 +159,24 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed)) self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed))
self.navigationItem.rightBarButtonItem = self.rightNavigationButton self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = false 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() { override func loadDisplayNode() {
self.displayNode = ContactMultiselectionControllerNode(context: self.context, mode: self.mode, options: self.options, filters: filters) 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.contactsNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: true, completion: nil) self?.presentingViewController?.dismiss(animated: true, completion: nil)
@ -174,7 +192,9 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
var displayCountAlert = false var displayCountAlert = false
var selectionState: ContactListNodeGroupSelectionState? var selectionState: ContactListNodeGroupSelectionState?
strongSelf.contactsNode.contactListNode.updateSelectionState { state in switch strongSelf.contactsNode.contentNode {
case let .contacts(contactsNode):
contactsNode.updateSelectionState { state in
if let state = state { if let state = state {
var updatedState = state.withToggledPeerId(.peer(peer.id)) var updatedState = state.withToggledPeerId(.peer(peer.id))
if updatedState.selectedPeerIndices[.peer(peer.id)] == nil { if updatedState.selectedPeerIndices[.peer(peer.id)] == nil {
@ -194,6 +214,26 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
return nil 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 {
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))
}
selectionState = updatedState
return state
}
break
}
if let searchResultsNode = strongSelf.contactsNode.searchResultsNode { if let searchResultsNode = strongSelf.contactsNode.searchResultsNode {
searchResultsNode.updateSelectionState { _ in searchResultsNode.updateSelectionState { _ in
return selectionState return selectionState
@ -202,7 +242,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
if let updatedCount = updatedCount { if let updatedCount = updatedCount {
switch strongSelf.mode { switch strongSelf.mode {
case .groupCreation, .peerSelection: case .groupCreation, .peerSelection, .chatSelection:
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0
case .channelCreation: case .channelCreation:
break break
@ -212,7 +252,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
case .groupCreation: case .groupCreation:
let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)") strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)")
case .peerSelection, .channelCreation: case .peerSelection, .channelCreation, .chatSelection:
break break
} }
} }
@ -238,7 +278,9 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
var removedTokenId: AnyHashable? var removedTokenId: AnyHashable?
var selectionState: ContactListNodeGroupSelectionState? var selectionState: ContactListNodeGroupSelectionState?
strongSelf.contactsNode.contactListNode.updateSelectionState { state in switch strongSelf.contactsNode.contentNode {
case let .contacts(contactsNode):
contactsNode.updateSelectionState { state in
if let state = state { if let state = state {
let updatedState = state.withToggledPeerId(peerId) let updatedState = state.withToggledPeerId(peerId)
if updatedState.selectedPeerIndices[peerId] == nil { if updatedState.selectedPeerIndices[peerId] == nil {
@ -253,6 +295,24 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
return nil 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 = state.selectedPeerIds.count
var updatedState = ContactListNodeGroupSelectionState()
for peerId in state.selectedPeerIds {
updatedState = updatedState.withToggledPeerId(.peer(peerId))
}
selectionState = updatedState
return state
}
}
if let searchResultsNode = strongSelf.contactsNode.searchResultsNode { if let searchResultsNode = strongSelf.contactsNode.searchResultsNode {
searchResultsNode.updateSelectionState { _ in searchResultsNode.updateSelectionState { _ in
return selectionState return selectionState
@ -261,7 +321,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
if let updatedCount = updatedCount { if let updatedCount = updatedCount {
switch strongSelf.mode { switch strongSelf.mode {
case .groupCreation, .peerSelection: case .groupCreation, .peerSelection, .chatSelection:
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0
case .channelCreation: case .channelCreation:
break break
@ -270,7 +330,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
case .groupCreation: case .groupCreation:
let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)") strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)")
case .peerSelection, .channelCreation: case .peerSelection, .channelCreation, .chatSelection:
break break
} }
} }
@ -290,7 +350,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) 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) { override func viewDidAppear(_ animated: Bool) {
@ -307,7 +372,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
override func viewDidDisappear(_ animated: Bool) { override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) 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) { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -323,12 +393,19 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
@objc func rightNavigationButtonPressed() { @objc func rightNavigationButtonPressed() {
var peerIds: [ContactListPeerId] = [] var peerIds: [ContactListPeerId] = []
self.contactsNode.contactListNode.updateSelectionState { state in switch self.contactsNode.contentNode {
case let .contacts(contactsNode):
contactsNode.updateSelectionState { state in
if let state = state { if let state = state {
peerIds = Array(state.selectedPeerIndices.keys) peerIds = Array(state.selectedPeerIndices.keys)
} }
return state return state
} }
case let .chats(chatsNode):
for peerId in chatsNode.currentState.selectedPeerIds {
peerIds.append(.peer(peerId))
}
}
self._result.set(.single(peerIds)) self._result.set(.single(peerIds))
} }
} }

View File

@ -9,6 +9,7 @@ import TelegramPresentationData
import MergeLists import MergeLists
import AccountContext import AccountContext
import ContactListUI import ContactListUI
import ChatListUI
private struct SearchResultEntry: Identifiable { private struct SearchResultEntry: Identifiable {
let index: Int 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 { final class ContactMultiselectionControllerNode: ASDisplayNode {
let contactListNode: ContactListNode let contentNode: ContactMultiselectionContentNode
let tokenListNode: EditableTokenListNode let tokenListNode: EditableTokenListNode
var searchResultsNode: ContactListNode? var searchResultsNode: ContactListNode?
@ -53,7 +68,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.context = context self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
let placeholder: String var placeholder: String
var includeChatList = false var includeChatList = false
switch mode { switch mode {
case let .peerSelection(_, searchGroups, searchChannels): case let .peerSelection(_, searchGroups, searchChannels):
@ -67,7 +82,13 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder 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) 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() super.init()
@ -78,12 +99,19 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.addSubnode(self.contactListNode) self.addSubnode(self.contentNode.node)
self.addSubnode(self.tokenListNode) self.addSubnode(self.tokenListNode)
self.contactListNode.openPeer = { [weak self] peer in switch self.contentNode {
case let .contacts(contactsNode):
contactsNode.openPeer = { [weak self] peer in
self?.openPeer?(peer) 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>() let searchText = ValuePromise<String>()
@ -102,10 +130,15 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
} else { } else {
if strongSelf.searchResultsNode == nil { if strongSelf.searchResultsNode == nil {
var selectionState: ContactListNodeGroupSelectionState? var selectionState: ContactListNodeGroupSelectionState?
strongSelf.contactListNode.updateSelectionState { state in switch strongSelf.contentNode {
case let .contacts(contactsNode):
contactsNode.updateSelectionState { state in
selectionState = state selectionState = state
return state return state
} }
case .chats:
break
}
var searchChatList = false var searchChatList = false
var searchGroups = false var searchGroups = false
var searchChannels = false var searchChannels = false
@ -113,6 +146,10 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
searchChatList = peerSelection.searchChatList searchChatList = peerSelection.searchChatList
searchGroups = peerSelection.searchGroups searchGroups = peerSelection.searchGroups
searchChannels = peerSelection.searchChannels 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) 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 searchResultsNode.openPeer = { peer in
@ -136,7 +173,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
strongSelf.searchResultsReadyDisposable.set((searchResultsNode.ready |> deliverOnMainQueue).start(next: { _ in strongSelf.searchResultsReadyDisposable.set((searchResultsNode.ready |> deliverOnMainQueue).start(next: { _ in
if let strongSelf = self, let searchResultsNode = strongSelf.searchResultsNode { 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 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) { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight) self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight)
@ -183,8 +229,15 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
insets.top += tokenListHeight insets.top += tokenListHeight
headerInsets.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) switch self.contentNode {
self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) 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 { 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) 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() { override func didLoad() {
super.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 { guard let strongSelf = self, let currentPaneKey = strongSelf.currentPaneKey, let availablePanes = strongSelf.currentParams?.data?.availablePanes, let index = availablePanes.index(of: currentPaneKey) else {
return [] return []
} }

View File

@ -85,7 +85,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.segmentedControlNode = nil 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() super.init()
@ -99,8 +99,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self?.requestActivateSearch?() self?.requestActivateSearch?()
} }
self.chatListNode.peerSelected = { [weak self] peerId, _, _ in self.chatListNode.peerSelected = { [weak self] peer, _, _ in
self?.requestOpenPeer?(peerId) self?.requestOpenPeer?(peer.id)
} }
self.chatListNode.disabledPeerSelected = { [weak self] peer in 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_SecureStorageReset_Title: String { return self._s[218]! }
public var Wallet_Receive_CommentHeader: String { return self._s[219]! } public var Wallet_Receive_CommentHeader: String { return self._s[219]! }
public var Wallet_Info_ReceiveGrams: String { return self._s[220]! } 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 form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator) let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue) 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 form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator) let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue) return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)