Update tab design

This commit is contained in:
Ali 2020-03-06 18:02:38 +04:00
parent c969f2556d
commit a29dc35c68
29 changed files with 2239 additions and 1042 deletions

View File

@ -120,6 +120,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
private var badgeDisposable: Disposable?
private var badgeIconDisposable: Disposable?
private var didAppear = false
private var dismissSearchOnDisappear = false
private var passcodeLockTooltipDisposable = MetaDisposable()
@ -133,7 +134,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
private var presentationDataDisposable: Disposable?
private let stateDisposable = MetaDisposable()
private var filterDisposable: Disposable?
private var filterDisposable = MetaDisposable()
private let isReorderingTabsValue = ValuePromise<Bool>(false)
private var searchContentNode: NavigationBarSearchContentNode?
@ -237,7 +240,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
strongSelf.chatListDisplayNode.containerNode.currentItemNode.scrollToPosition(.top)
case let .known(offset):
if offset <= navigationBarSearchContentHeight + 1.0 {
if offset <= navigationBarSearchContentHeight + 1.0 && strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter != nil {
strongSelf.tabContainerNode.tabSelected?(.all)
} else {
if let searchContentNode = strongSelf.searchContentNode {
@ -272,8 +275,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
context.account.networkState,
hasProxy,
passcode,
self.chatListDisplayNode.containerNode.currentItemState
).start(next: { [weak self] networkState, proxy, passcode, state in
self.chatListDisplayNode.containerNode.currentItemState,
self.isReorderingTabsValue.get()
).start(next: { [weak self] networkState, proxy, passcode, state, isReorderingTabs in
if let strongSelf = self {
let defaultTitle: String
if strongSelf.groupId == .root {
@ -292,9 +296,29 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
var isRoot = false
if case .root = strongSelf.groupId {
isRoot = true
let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed))
rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose
strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem
if isReorderingTabs {
let rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.reorderingDonePressed))
strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem
} else {
let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed))
rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose
strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem
}
if isReorderingTabs {
strongSelf.navigationItem.leftBarButtonItem = nil
} else {
let editItem: UIBarButtonItem
if state.editing {
editItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(strongSelf.donePressed))
editItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Done
} else {
editItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(strongSelf.editPressed))
editItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Edit
}
strongSelf.navigationItem.leftBarButtonItem = editItem
}
}
let (hasProxy, connectsViaProxy) = proxy
@ -397,7 +421,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
}
if self.filter == nil {
if self.filter == nil, case .root = self.groupId {
self.chatListDisplayNode.containerNode.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in
guard let strongSelf = self else {
return
@ -411,67 +435,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
if force {
strongSelf.tabContainerNode.cancelAnimations()
}
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)
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, isEditing: false, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition)
}
let preferencesKey: PostboxViewKey = .preferences(keys: Set([
ApplicationSpecificPreferencesKeys.chatListFilterSettings
]))
let filterItems = chatListFilterItems(context: context)
self.filterDisposable = (combineLatest(queue: .mainQueue(),
context.account.postbox.combinedView(keys: [
preferencesKey
]),
filterItems
)
|> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems in
guard let strongSelf = self else {
return
}
let (_, items) = countAndFilterItems
var filterItems: [ChatListFilterTabEntry] = []
filterItems.append(.all(unreadCount: 0))
for (filter, unreadCount) in items {
filterItems.append(.filter(id: filter.id, text: filter.title, unreadCount: unreadCount))
}
var resolvedItems = filterItems
if groupId != .root {
resolvedItems = []
}
var wasEmpty = false
if let tabContainerData = strongSelf.tabContainerData {
wasEmpty = tabContainerData.count <= 1
} else {
wasEmpty = true
}
let selectedEntryId = strongSelf.chatListDisplayNode.containerNode.currentItemFilter
strongSelf.tabContainerData = resolvedItems
var availableFilters: [ChatListContainerNodeFilter] = []
availableFilters.append(.all)
for item in items {
availableFilters.append(.filter(item.0))
}
strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters)
let isEmpty = resolvedItems.count <= 1
if wasEmpty != isEmpty {
strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode)
if let parentController = strongSelf.parent as? TabBarController {
parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode)
}
}
if let layout = strongSelf.validLayout {
if wasEmpty != isEmpty {
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
(strongSelf.parent as? TabBarController)?.updateLayout()
} else {
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))
}
}
})
self.reloadFilters()
}
self.tabContainerNode.tabSelected = { [weak self] id in
@ -510,6 +476,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
})
}
self.tabContainerNode.tabRequestedDeletion = { [weak self] id in
if case let .filter(id) = id {
self?.askForFilterRemoval(id: id)
}
}
self.tabContainerNode.addFilter = { [weak self] in
self?.openFilterSettings()
}
@ -527,7 +499,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Edit, icon: { theme in
//TODO:localization
items.append(.action(ContextMenuActionItem(text: "Edit Filter", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
}, action: { c, f in
c.dismiss(completion: {
@ -597,34 +570,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: "This will remove the filter, your chats will not be deleted."),
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))
strongSelf.askForFilterRemoval(id: id)
})
})))
if filters.count > 1 {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: "Reorder Tabs", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { c, f in
//f(.default)
c.dismiss(completion: {
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.isReorderingFilters = true
strongSelf.isReorderingTabsValue.set(true)
strongSelf.searchContentNode?.setIsEnabled(false, animated: true)
if let layout = strongSelf.validLayout {
strongSelf.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
})
})))
}
}
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)
@ -646,7 +615,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.suggestLocalizationDisposable.dispose()
self.presentationDataDisposable?.dispose()
self.stateDisposable.dispose()
self.filterDisposable?.dispose()
self.filterDisposable.dispose()
}
private func updateThemeAndStrings() {
@ -686,7 +655,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
if let layout = self.validLayout {
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: .immediate)
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, isEditing: false, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate)
}
if self.isNodeLoaded {
@ -879,10 +848,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
}
self.chatListDisplayNode.dismissSelf = { [weak self] in
self.chatListDisplayNode.dismissSelfIfCompletedPresentation = { [weak self] in
guard let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
if !strongSelf.didAppear {
return
}
navigationController.filterController(strongSelf, animated: true)
}
@ -1025,6 +997,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.didAppear = true
guard case .root = self.groupId else {
return
}
@ -1119,7 +1093,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
if let _ = self.validLayout, let parentController = self.parent as? TabBarController, let sourceFrame = parentController.frameForControllerTab(controller: self) {
let absoluteFrame = sourceFrame
//TODO:localize
parentController.present(TooltipScreen(text: "Hold the Chats icon for quick access to the list of chat filters.", location: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0)), in: .current)
//parentController.present(TooltipScreen(text: "Hold the Chats icon for quick access to the list of chat filters.", location: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0)), in: .current)
}
})
}
@ -1174,6 +1148,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.validLayout = layout
self.updateLayout(layout: layout, transition: transition)
if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver {
searchContentNode.updateListVisibleContentOffset(.known(0.0))
self.chatListDisplayNode.scrollToTop()
}
}
private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
var tabContainerOffset: CGFloat = 0.0
if !self.displayNavigationBar {
tabContainerOffset += layout.statusBarHeight ?? 0.0
@ -1181,12 +1164,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.visualNavigationInsetHeight - self.additionalHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0)))
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver {
searchContentNode.updateListVisibleContentOffset(.known(0.0))
self.chatListDisplayNode.scrollToTop()
}
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, isEditing: false, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, visualNavigationHeight: self.visualNavigationInsetHeight, cleanNavigationBarHeight: self.cleanNavigationHeight, transition: transition)
}
@ -1213,9 +1191,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
state.peerIdWithRevealedOptions = nil
return state
}
self.chatListDisplayNode.isEditing = true
if let layout = self.validLayout {
self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
@objc private func donePressed() {
self.reorderingDonePressed()
let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
editItem.accessibilityLabel = self.presentationData.strings.Common_Edit
if case .root = self.groupId, self.filter == nil {
@ -1232,6 +1216,173 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
state.selectedPeerIds.removeAll()
return state
}
self.chatListDisplayNode.isEditing = false
if let layout = self.validLayout {
self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
@objc private func reorderingDonePressed() {
if let reorderedFilterIds = self.tabContainerNode.reorderedFilterIds {
let _ = (updateChatListFilterSettingsInteractively(postbox: self.context.account.postbox, { state in
var state = state
var updatedFilters: [ChatListFilter] = []
for id in reorderedFilterIds {
if let index = state.filters.firstIndex(where: { $0.id == id }) {
updatedFilters.append(state.filters[index])
}
}
updatedFilters.append(contentsOf: state.filters.compactMap { filter -> ChatListFilter? in
if !updatedFilters.contains(where: { $0.id == filter.id }) {
return filter
} else {
return nil
}
})
state.filters = updatedFilters
return state
})
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
let _ = replaceRemoteChatListFilters(account: strongSelf.context.account).start()
strongSelf.reloadFilters(firstUpdate: {
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.isReorderingFilters = false
strongSelf.isReorderingTabsValue.set(false)
strongSelf.searchContentNode?.setIsEnabled(true, animated: true)
if let layout = strongSelf.validLayout {
strongSelf.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
})
})
}
}
private func reloadFilters(firstUpdate: (() -> Void)? = nil) {
let preferencesKey: PostboxViewKey = .preferences(keys: Set([
ApplicationSpecificPreferencesKeys.chatListFilterSettings
]))
let filterItems = chatListFilterItems(context: self.context)
var notifiedFirstUpdate = false
self.filterDisposable.set((combineLatest(queue: .mainQueue(),
context.account.postbox.combinedView(keys: [
preferencesKey
]),
filterItems
)
|> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems in
guard let strongSelf = self else {
return
}
let (_, items) = countAndFilterItems
var filterItems: [ChatListFilterTabEntry] = []
filterItems.append(.all(unreadCount: 0))
for (filter, unreadCount) in items {
filterItems.append(.filter(id: filter.id, text: filter.title, unreadCount: unreadCount))
}
var resolvedItems = filterItems
if strongSelf.groupId != .root {
resolvedItems = []
}
var wasEmpty = false
if let tabContainerData = strongSelf.tabContainerData {
wasEmpty = tabContainerData.count <= 1
} else {
wasEmpty = true
}
var selectedEntryId = strongSelf.chatListDisplayNode.containerNode.currentItemFilter
var resetCurrentEntry = false
if !resolvedItems.contains(where: { $0.id == selectedEntryId }) {
resetCurrentEntry = true
if let tabContainerData = strongSelf.tabContainerData {
var found = false
if let index = tabContainerData.firstIndex(where: { $0.id == selectedEntryId }) {
for i in (0 ..< index - 1).reversed() {
if resolvedItems.contains(where: { $0.id == tabContainerData[i].id }) {
selectedEntryId = tabContainerData[i].id
found = true
break
}
}
}
if !found {
selectedEntryId = .all
}
} else {
selectedEntryId = .all
}
}
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
if wasEmpty != isEmpty {
strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode)
if let parentController = strongSelf.parent as? TabBarController {
parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode)
}
}
if let layout = strongSelf.validLayout {
if wasEmpty != isEmpty {
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
(strongSelf.parent as? TabBarController)?.updateLayout()
} else {
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring))
}
}
if !notifiedFirstUpdate {
notifiedFirstUpdate = true
firstUpdate?()
}
if resetCurrentEntry {
strongSelf.tabContainerNode.tabSelected?(selectedEntryId)
}
}))
}
private func askForFilterRemoval(id: Int32) {
let actionSheet = ActionSheetController(presentationData: self.presentationData)
//TODO:localization
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: "This will remove the filter, your chats will not be deleted."),
ActionSheetButtonItem(title: "Remove", color: .destructive, action: { [weak self, 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: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.present(actionSheet, in: .window(.root))
}
public func activateSearch() {
@ -1457,7 +1608,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} else {
let groupId = self.groupId
signal = self.context.account.postbox.transaction { transaction -> Void in
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: (self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data).flatMap(chatListFilterPredicate))
let filterPredicate = (self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data).flatMap(chatListFilterPredicate)
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: filterPredicate)
if let filterPredicate = filterPredicate {
for additionalGroupId in filterPredicate.includeAdditionalPeerGroupIds {
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: additionalGroupId, filterPredicate: filterPredicate)
}
}
}
}
let _ = (signal
@ -2155,7 +2312,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} else {
if preset.data.categories == .channels {
filterType = .channels
} else if preset.data.categories.isSubset(of: [.smallGroups, .largeGroups]) {
} else if preset.data.categories == .groups {
filterType = .groups
} else if preset.data.categories == .bots {
filterType = .bots

View File

@ -147,7 +147,9 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
private(set) var transitionFraction: CGFloat = 0.0
private var transitionFractionOffset: CGFloat = 0.0
private var disableItemNodeOperationsWhileAnimating: Bool = false
private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat)?
private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, isReorderingFilters: Bool, isEditing: Bool)?
private var panRecognizer: InteractiveTransitionGestureRecognizer?
private let _ready = Promise<Bool>()
var ready: Signal<Bool, NoError> {
@ -287,6 +289,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.panRecognizer = panRecognizer
self.view.addGestureRecognizer(panRecognizer)
}
@ -312,7 +315,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
switch recognizer.state {
case .began:
self.transitionFractionOffset = 0.0
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let itemNode = self.itemNodes[self.selectedId] {
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout, let itemNode = self.itemNodes[self.selectedId] {
if let presentationLayer = itemNode.layer.presentation() {
self.transitionFraction = presentationLayer.frame.minX / layout.size.width
self.transitionFractionOffset = self.transitionFraction
@ -320,13 +323,13 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
for (_, itemNode) in self.itemNodes {
itemNode.layer.removeAllAnimations()
}
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, true)
}
}
}
case .changed:
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
let translation = recognizer.translation(in: self.view)
var transitionFraction = translation.x / layout.size.width
if selectedIndex <= 0 {
@ -344,11 +347,11 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
}
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, false)
}
case .cancelled, .ended:
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
let translation = recognizer.translation(in: self.view)
let velocity = recognizer.velocity(in: self.view)
var directionIsToRight: Bool?
@ -387,12 +390,12 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
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.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: transition)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false)
DispatchQueue.main.async {
self.disableItemNodeOperationsWhileAnimating = false
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout {
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout {
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate)
}
}
}
@ -449,14 +452,14 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
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)
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout {
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate)
}
}
}
func switchToFilter(id: ChatListFilterTabEntryId) {
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout else {
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout else {
return
}
if id != self.selectedId, let index = self.availableFilters.firstIndex(where: { $0.id == id }) {
@ -467,7 +470,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
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.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: transition)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false)
} 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
@ -485,7 +488,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else {
return
}
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = strongSelf.validLayout else {
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = strongSelf.validLayout else {
return
}
strongSelf.pendingItemNode = nil
@ -542,7 +545,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: isReorderingFilters, isEditing: isEditing, transition: .immediate)
strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, transition, false)
}
@ -551,8 +554,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight)
func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, isReorderingFilters: Bool, isEditing: Bool, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
@ -560,6 +563,11 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
transition.updateAlpha(node: self, alpha: isReorderingFilters ? 0.5 : 1.0)
self.isUserInteractionEnabled = !isReorderingFilters
self.panRecognizer?.isEnabled = !isEditing
if let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
var validNodeIds: [ChatListFilterTabEntryId] = []
for i in max(0, selectedIndex - 1) ... min(self.availableFilters.count - 1, selectedIndex + 1) {
@ -642,6 +650,9 @@ final class ChatListControllerNode: ASDisplayNode {
private(set) var searchDisplayController: SearchDisplayController?
var isReorderingFilters: Bool = false
var isEditing: Bool = false
private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat)?
var requestDeactivateSearch: (() -> Void)?
@ -650,7 +661,7 @@ final class ChatListControllerNode: ASDisplayNode {
var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)?
var requestAddContact: ((String) -> Void)?
var peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?
var dismissSelf: (() -> Void)?
var dismissSelfIfCompletedPresentation: (() -> Void)?
var isEmptyUpdated: ((Bool) -> Void)?
var emptyListAction: (() -> Void)?
@ -688,7 +699,7 @@ final class ChatListControllerNode: ASDisplayNode {
return
}
if case .group = strongSelf.groupId {
strongSelf.dismissSelf?()
strongSelf.dismissSelfIfCompletedPresentation?()
}
}
filterEmptyAction = { [weak self] filter in
@ -771,7 +782,7 @@ final class ChatListControllerNode: ASDisplayNode {
}
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.containerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition)
self.containerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, transition: transition)
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: transition)

View File

@ -13,8 +13,7 @@ import ChatListSearchItemHeader
enum ChatListFilterCategoryIcon {
case contacts
case nonContacts
case smallGroups
case largeGroups
case groups
case channels
case bots
case muted
@ -245,12 +244,9 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
case .nonContacts:
color = .yellow
imageName = "Chat/Context Menu/UnknownUser"
case .smallGroups:
case .groups:
color = .green
imageName = "Chat/Context Menu/Groups"
case .largeGroups:
color = .purple
imageName = "Chat/Context Menu/LargeGroup"
case .channels:
color = .red
imageName = "Chat/Context Menu/Channels"

View File

@ -6,6 +6,7 @@ import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import PresentationDataUtils
import ItemListUI
import AccountContext
import TelegramUIPreferences
@ -51,6 +52,7 @@ private final class ChatListFilterPresetControllerArguments {
}
private enum ChatListFilterPresetControllerSection: Int32 {
case screenHeader
case name
case includePeers
case excludePeers
@ -66,14 +68,24 @@ private enum ChatListFilterPresetEntryStableId: Hashable {
}
private enum ChatListFilterPresetEntrySortId: Comparable {
case screenHeader
case topIndex(Int)
case includeIndex(Int)
case excludeIndex(Int)
static func <(lhs: ChatListFilterPresetEntrySortId, rhs: ChatListFilterPresetEntrySortId) -> Bool {
switch lhs {
case .screenHeader:
switch rhs {
case .screenHeader:
return false
default:
return true
}
case let .topIndex(lhsIndex):
switch rhs {
case .screenHeader:
return false
case let .topIndex(rhsIndex):
return lhsIndex < rhsIndex
case .includeIndex:
@ -83,6 +95,8 @@ private enum ChatListFilterPresetEntrySortId: Comparable {
}
case let .includeIndex(lhsIndex):
switch rhs {
case .screenHeader:
return false
case .topIndex:
return false
case let .includeIndex(rhsIndex):
@ -92,6 +106,8 @@ private enum ChatListFilterPresetEntrySortId: Comparable {
}
case let .excludeIndex(lhsIndex):
switch rhs {
case .screenHeader:
return false
case .topIndex:
return false
case .includeIndex:
@ -106,8 +122,7 @@ private enum ChatListFilterPresetEntrySortId: Comparable {
private enum ChatListFilterIncludeCategory: Int32, CaseIterable {
case contacts
case nonContacts
case smallGroups
case largeGroups
case groups
case channels
case bots
@ -117,10 +132,8 @@ private enum ChatListFilterIncludeCategory: Int32, CaseIterable {
return .contacts
case .nonContacts:
return .nonContacts
case .smallGroups:
return .smallGroups
case .largeGroups:
return .largeGroups
case .groups:
return .groups
case .channels:
return .channels
case .bots:
@ -134,10 +147,8 @@ private enum ChatListFilterIncludeCategory: Int32, CaseIterable {
return "Contacts"
case .nonContacts:
return "Non-Contacts"
case .smallGroups:
return "Small Groups"
case .largeGroups:
return "Large Groups"
case .groups:
return "Groups"
case .channels:
return "Channels"
case .bots:
@ -170,10 +181,8 @@ private extension ChatListFilterCategoryIcon {
self = .contacts
case .nonContacts:
self = .nonContacts
case .smallGroups:
self = .smallGroups
case .largeGroups:
self = .largeGroups
case .groups:
self = .groups
case .channels:
self = .channels
case .bots:
@ -200,6 +209,7 @@ private enum ChatListFilterRevealedItemId: Equatable {
}
private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case screenHeader
case nameHeader(String)
case name(placeholder: String, value: String)
case includePeersHeader(String)
@ -215,6 +225,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .screenHeader:
return ChatListFilterPresetControllerSection.screenHeader.rawValue
case .nameHeader, .name:
return ChatListFilterPresetControllerSection.name.rawValue
case .includePeersHeader, .addIncludePeer, .includeCategory, .includePeer, .includePeerInfo:
@ -226,26 +238,28 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
var stableId: ChatListFilterPresetEntryStableId {
switch self {
case .nameHeader:
case .screenHeader:
return .index(0)
case .name:
case .nameHeader:
return .index(1)
case .includePeersHeader:
case .name:
return .index(2)
case .addIncludePeer:
case .includePeersHeader:
return .index(3)
case .addIncludePeer:
return .index(4)
case let .includeCategory(includeCategory):
return .includeCategory(includeCategory.category)
case .includePeerInfo:
return .index(4)
case .excludePeersHeader:
return .index(5)
case .addExcludePeer:
case .excludePeersHeader:
return .index(6)
case .addExcludePeer:
return .index(7)
case let .excludeCategory(excludeCategory):
return .excludeCategory(excludeCategory.category)
case .excludePeerInfo:
return .index(7)
return .index(8)
case let .includePeer(peer):
return .peer(peer.peer.peerId)
case let .excludePeer(peer):
@ -255,6 +269,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
private var sortIndex: ChatListFilterPresetEntrySortId {
switch self {
case .screenHeader:
return .screenHeader
case .nameHeader:
return .topIndex(0)
case .name:
@ -289,6 +305,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChatListFilterPresetControllerArguments
switch self {
case .screenHeader:
return ChatListFilterSettingsHeaderItem(theme: presentationData.theme, text: "", animation: .newFolder, sectionId: self.section)
case let .nameHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .name(placeholder, value):
@ -384,14 +402,35 @@ private struct ChatListFilterPresetControllerState: Equatable {
if self.name.isEmpty {
return false
}
let defaultCategories: ChatListFilterPeerCategories = .all
let defaultExcludeArchived = true
let defaultExcludeMuted = false
let defaultExcludeRead = false
if self.includeCategories == defaultCategories &&
self.excludeArchived == defaultExcludeArchived &&
self.excludeMuted == defaultExcludeMuted &&
self.excludeRead == defaultExcludeRead {
return false
}
if self.includeCategories.isEmpty && self.additionallyIncludePeers.isEmpty {
return false
}
return true
}
}
//TODO:localization
private func chatListFilterPresetControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetControllerState, includePeers: [RenderedPeer], excludePeers: [RenderedPeer]) -> [ChatListFilterPresetEntry] {
private func chatListFilterPresetControllerEntries(presentationData: PresentationData, isNewFilter: Bool, state: ChatListFilterPresetControllerState, includePeers: [RenderedPeer], excludePeers: [RenderedPeer]) -> [ChatListFilterPresetEntry] {
var entries: [ChatListFilterPresetEntry] = []
if isNewFilter {
entries.append(.screenHeader)
}
entries.append(.nameHeader("FILTER NAME"))
entries.append(.name(placeholder: "Filter Name", value: state.name))
@ -445,8 +484,7 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
private enum AdditionalCategoryId: Int {
case contacts
case nonContacts
case smallGroups
case largeGroups
case groups
case channels
case bots
}
@ -474,14 +512,9 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
title: "Non-Contacts"
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.smallGroups.rawValue,
id: AdditionalCategoryId.groups.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Groups"), color: .white), color: .green),
title: "Small Groups"
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.largeGroups.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/LargeGroup"), color: .white), color: .purple),
title: "Large Groups"
title: "Groups"
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.channels.rawValue,
@ -498,8 +531,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
let categoryMapping: [ChatListFilterPeerCategories: AdditionalCategoryId] = [
.contacts: .contacts,
.nonContacts: .nonContacts,
.smallGroups: .smallGroups,
.largeGroups: .largeGroups,
.groups: .groups,
.channels: .channels,
.bots: .bots
]
@ -658,7 +690,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} else {
initialName = "New Filter"
}
let initialState = ChatListFilterPresetControllerState(name: initialName, includeCategories: currentPreset?.data.categories ?? .all, excludeMuted: currentPreset?.data.excludeMuted ?? false, excludeRead: currentPreset?.data.excludeRead ?? false, excludeArchived: currentPreset?.data.excludeArchived ?? false, additionallyIncludePeers: currentPreset?.data.includePeers ?? [], additionallyExcludePeers: currentPreset?.data.excludePeers ?? [])
let initialState = ChatListFilterPresetControllerState(name: initialName, includeCategories: currentPreset?.data.categories ?? [], excludeMuted: currentPreset?.data.excludeMuted ?? false, excludeRead: currentPreset?.data.excludeRead ?? false, excludeArchived: currentPreset?.data.excludeArchived ?? false, additionallyIncludePeers: currentPreset?.data.includePeers ?? [], additionallyExcludePeers: currentPreset?.data.excludePeers ?? [])
let stateValue = Atomic(value: initialState)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in
@ -816,6 +848,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
}
var attemptNavigationImpl: (() -> Bool)?
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
stateWithPeers
@ -825,7 +859,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
let (state, includePeers, excludePeers) = stateWithPeers
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
let _ = attemptNavigationImpl?()
})
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 }
@ -845,6 +879,12 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
}
if !found {
settings.filters = settings.filters.filter { listFilter in
if listFilter.title == preset.title && listFilter.data == preset.data {
return false
}
return true
}
settings.filters.append(preset)
}
} else {
@ -861,7 +901,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
})
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(currentPreset != nil ? "Filter" : "Create Filter"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, state: state, includePeers: includePeers, excludePeers: excludePeers), style: .blocks, emptyStateItem: nil, animateChanges: !skipStateAnimation)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, isNewFilter: currentPreset == nil, state: state, includePeers: includePeers, excludePeers: excludePeers), style: .blocks, emptyStateItem: nil, animateChanges: !skipStateAnimation)
skipStateAnimation = false
return (controllerState, (listState, arguments))
@ -888,6 +928,31 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
}
}
controller.attemptNavigation = { _ in
return attemptNavigationImpl?() ?? true
}
let displaySaveAlert: () -> Void = {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: "Are you sure you want to discard this folder?", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: {
dismissImpl?()
})]), nil)
}
attemptNavigationImpl = {
let state = stateValue.with { $0 }
if let currentPreset = currentPreset {
let filter = ChatListFilter(id: currentPreset.id, title: state.name, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: state.additionallyIncludePeers, excludePeers: state.additionallyExcludePeers))
if currentPreset != filter {
displaySaveAlert()
return false
}
} else {
if state.isComplete {
displaySaveAlert()
return false
}
}
return true
}
return controller
}

View File

@ -124,7 +124,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
let arguments = arguments as! ChatListFilterPresetListControllerArguments
switch self {
case let .screenHeader(text):
return ChatListFilterSettingsHeaderItem(theme: presentationData.theme, text: text, sectionId: self.section)
return ChatListFilterSettingsHeaderItem(theme: presentationData.theme, text: text, animation: .folders, sectionId: self.section)
case let .suggestedListHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .suggestedPreset(_, title, label, preset):
@ -272,7 +272,7 @@ public func chatListFilterPresetListController(context: AccountContext, updated:
let suggestedFilters: [(String, String, ChatListFilterData)] = [
("Unread", "All unread chats", ChatListFilterData(categories: .all, excludeMuted: false, excludeRead: true, excludeArchived: false, includePeers: [], excludePeers: [])),
("Personal", "Exclude large groups and channels", ChatListFilterData(categories: ChatListFilterPeerCategories.all.subtracting([.largeGroups, .channels]), excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: [], excludePeers: [])),
("Personal", "Exclude groups and channels", ChatListFilterData(categories: ChatListFilterPeerCategories.all.subtracting([.groups, .channels]), excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: [], excludePeers: [])),
]
let signal = combineLatest(queue: .mainQueue(),

View File

@ -351,7 +351,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size))
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width, y: 11.0), size: labelLayout.size)
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: 11.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
@ -362,7 +362,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}
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.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width + revealOffset, 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))
@ -432,6 +432,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}
let leftInset: CGFloat = 16.0 + params.leftInset
let rightArrowInset: CGFloat = 34.0 + params.rightInset
let editingOffset: CGFloat
if let editableControlNode = self.editableControlNode {
@ -444,6 +445,14 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
var labelFrame = self.labelNode.frame
labelFrame.origin.x = params.width - rightArrowInset - labelFrame.width + revealOffset
transition.updateFrame(node: self.labelNode, frame: labelFrame)
var arrowFrame = self.arrowNode.frame
arrowFrame.origin.x = params.width - params.rightInset - 7.0 - arrowFrame.width + revealOffset
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
override func revealOptionsInteractivelyOpened() {

View File

@ -9,14 +9,21 @@ import PresentationDataUtils
import AnimatedStickerNode
import AppBundle
enum ChatListFilterSettingsHeaderAnimation {
case folders
case newFolder
}
class ChatListFilterSettingsHeaderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let text: String
let animation: ChatListFilterSettingsHeaderAnimation
let sectionId: ItemListSectionId
init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId) {
init(theme: PresentationTheme, text: String, animation: ChatListFilterSettingsHeaderAnimation, sectionId: ItemListSectionId) {
self.theme = theme
self.text = text
self.animation = animation
self.sectionId = sectionId
}
@ -72,10 +79,6 @@ class ChatListFilterSettingsHeaderItemNode: ListViewItemNode {
self.titleNode.contentsScale = UIScreen.main.scale
self.animationNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "ChatListFolders", ofType: "tgs") {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
self.animationNode.visibility = true
}
super.init(layerBacked: false, dynamicBounce: false)
@ -100,6 +103,20 @@ class ChatListFilterSettingsHeaderItemNode: ListViewItemNode {
return (layout, { [weak self] in
if let strongSelf = self {
if strongSelf.item == nil {
let animationName: String
switch item.animation {
case .folders:
animationName = "ChatListFolders"
case .newFolder:
animationName = "ChatListNewFolder"
}
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
strongSelf.animationNode.visibility = true
}
}
strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string

View File

@ -7,36 +7,94 @@ import Postbox
import TelegramCore
import TelegramPresentationData
private final class ItemNodeDeleteButtonNode: HighlightableButtonNode {
private let pressed: () -> Void
private let contentImageNode: ASImageNode
private var theme: PresentationTheme?
init(pressed: @escaping () -> Void) {
self.pressed = pressed
self.contentImageNode = ASImageNode()
super.init()
self.addSubnode(self.contentImageNode)
self.addTarget(self, action: #selector(self.pressedEvent), forControlEvents: .touchUpInside)
}
@objc private func pressedEvent() {
self.pressed()
}
func update(theme: PresentationTheme) -> CGSize {
let size = CGSize(width: 18.0, height: 18.0)
if self.theme !== theme {
self.theme = theme
self.contentImageNode.image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.rootController.navigationBar.clearButtonBackgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.rootController.navigationBar.clearButtonForegroundColor.cgColor)
context.setLineWidth(1.5)
context.setLineCap(.round)
context.move(to: CGPoint(x: 6.38, y: 6.38))
context.addLine(to: CGPoint(x: 11.63, y: 11.63))
context.strokePath()
context.move(to: CGPoint(x: 6.38, y: 11.63))
context.addLine(to: CGPoint(x: 11.63, y: 6.38))
context.strokePath()
})
}
self.contentImageNode.frame = CGRect(origin: CGPoint(), size: size)
return size
}
}
private final class ItemNode: ASDisplayNode {
private let pressed: () -> Void
private let requestedDeletion: () -> Void
private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let extractedBackgroundNode: ASImageNode
private let titleNode: ImmediateTextNode
private let extractedTitleNode: ImmediateTextNode
private let shortTitleNode: ImmediateTextNode
private let badgeContainerNode: ASDisplayNode
private let badgeTextNode: ImmediateTextNode
private let badgeBackgroundNode: ASImageNode
private var deleteButtonNode: ItemNodeDeleteButtonNode?
private let buttonNode: HighlightTrackingButtonNode
private var isSelected: Bool = false
private(set) var unreadCount: Int = 0
private var isReordering: Bool = false
private var theme: PresentationTheme?
init(pressed: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) {
init(pressed: @escaping () -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) {
self.pressed = pressed
self.requestedDeletion = requestedDeletion
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundNode = ASImageNode()
self.extractedBackgroundNode.alpha = 0.0
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.extractedTitleNode = ImmediateTextNode()
self.extractedTitleNode.displaysAsynchronously = false
self.extractedTitleNode.alpha = 0.0
self.shortTitleNode = ImmediateTextNode()
self.shortTitleNode.displaysAsynchronously = false
self.shortTitleNode.alpha = 0.0
self.badgeContainerNode = ASDisplayNode()
@ -51,8 +109,9 @@ private final class ItemNode: ASDisplayNode {
super.init()
self.extractedContainerNode.contentNode.addSubnode(self.extractedBackgroundNode)
self.extractedContainerNode.contentNode.addSubnode(self.titleNode)
self.extractedContainerNode.contentNode.addSubnode(self.extractedTitleNode)
self.extractedContainerNode.contentNode.addSubnode(self.shortTitleNode)
self.badgeContainerNode.addSubnode(self.badgeBackgroundNode)
self.badgeContainerNode.addSubnode(self.badgeTextNode)
self.extractedContainerNode.contentNode.addSubnode(self.badgeContainerNode)
@ -75,8 +134,15 @@ private final class ItemNode: ASDisplayNode {
guard let strongSelf = self else {
return
}
transition.updateAlpha(node: strongSelf.titleNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.extractedTitleNode, alpha: isExtracted ? 1.0 : 0.0)
if isExtracted, let theme = strongSelf.theme {
strongSelf.extractedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: theme.contextMenu.backgroundColor)
}
transition.updateAlpha(node: strongSelf.extractedBackgroundNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundNode.image = nil
}
})
}
}
@ -84,30 +150,73 @@ private final class ItemNode: ASDisplayNode {
self.pressed()
}
func updateText(title: String, unreadCount: Int, isNoFilter: Bool, isSelected: Bool, presentationData: PresentationData) {
func updateText(title: String, shortTitle: String, unreadCount: Int, isNoFilter: Bool, isSelected: Bool, isEditing: Bool, isAllChats: Bool, isReordering: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
if self.theme !== presentationData.theme {
self.theme = presentationData.theme
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.list.itemCheckColors.fillColor)
}
self.containerNode.isGestureEnabled = !isNoFilter
self.containerNode.isGestureEnabled = !isNoFilter && !isEditing && !isReordering
self.buttonNode.isUserInteractionEnabled = !isEditing && !isReordering
self.isSelected = isSelected
self.unreadCount = unreadCount
transition.updateAlpha(node: self.containerNode, alpha: isReordering && isAllChats ? 0.5 : 1.0)
if isReordering && !isAllChats {
if self.deleteButtonNode == nil {
let deleteButtonNode = ItemNodeDeleteButtonNode(pressed: { [weak self] in
self?.requestedDeletion()
})
self.extractedContainerNode.contentNode.addSubnode(deleteButtonNode)
self.deleteButtonNode = deleteButtonNode
if case .animated = transition {
deleteButtonNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.25)
deleteButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
} else if let deleteButtonNode = self.deleteButtonNode {
self.deleteButtonNode = nil
transition.updateTransformScale(node: deleteButtonNode, scale: 0.1)
transition.updateAlpha(node: deleteButtonNode, alpha: 0.0, completion: { [weak deleteButtonNode] _ in
deleteButtonNode?.removeFromSupernode()
})
}
transition.updateAlpha(node: self.badgeContainerNode, alpha: (isReordering || unreadCount == 0) ? 0.0 : 1.0)
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor)
self.extractedTitleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.contextMenu.extractedContentTintColor)
self.shortTitleNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor)
if unreadCount != 0 {
self.badgeTextNode.attributedText = NSAttributedString(string: "\(unreadCount)", font: Font.regular(14.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
}
if self.isReordering != isReordering {
self.isReordering = isReordering
if self.isReordering && !isAllChats {
self.startShaking()
} else {
self.layer.removeAnimation(forKey: "shaking_position")
self.layer.removeAnimation(forKey: "shaking_rotation")
}
}
}
func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> (width: CGFloat, shortWidth: CGFloat) {
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
let _ = self.extractedTitleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
self.extractedTitleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
let shortTitleSize = self.shortTitleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
self.shortTitleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - shortTitleSize.height) / 2.0)), size: shortTitleSize)
if let deleteButtonNode = self.deleteButtonNode {
if let theme = self.theme {
let deleteButtonSize = deleteButtonNode.update(theme: theme)
deleteButtonNode.frame = CGRect(origin: CGPoint(x: -deleteButtonSize.width, y: 5.0), size: deleteButtonSize)
}
}
let badgeSize = self.badgeTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
let badgeInset: CGFloat = 4.0
@ -116,21 +225,35 @@ private final class ItemNode: ASDisplayNode {
self.badgeBackgroundNode.frame = CGRect(origin: CGPoint(), size: badgeBackgroundFrame.size)
self.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((badgeBackgroundFrame.width - badgeSize.width) / 2.0), y: floor((badgeBackgroundFrame.height - badgeSize.height) / 2.0)), size: badgeSize)
if self.unreadCount == 0 {
self.badgeContainerNode.alpha = 0.0
return titleSize.width
let width: CGFloat
if self.unreadCount == 0 || self.isReordering {
if !self.isReordering {
self.badgeContainerNode.alpha = 0.0
}
width = titleSize.width
} else {
self.badgeContainerNode.alpha = 1.0
return badgeBackgroundFrame.maxX
if !self.isReordering {
self.badgeContainerNode.alpha = 1.0
}
width = badgeBackgroundFrame.maxX
}
let extractedBackgroundHeight: CGFloat = 36.0
let extractedBackgroundInset: CGFloat = 14.0
self.extractedBackgroundNode.frame = CGRect(origin: CGPoint(x: -extractedBackgroundInset, y: floor((height - extractedBackgroundHeight) / 2.0)), size: CGSize(width: width + extractedBackgroundInset * 2.0, height: extractedBackgroundHeight))
return (width, shortTitleSize.width)
}
func updateArea(size: CGSize, sideInset: CGFloat) {
func updateArea(size: CGSize, sideInset: CGFloat, useShortTitle: Bool, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.titleNode, alpha: useShortTitle ? 0.0 : 1.0)
transition.updateAlpha(node: self.shortTitleNode, alpha: useShortTitle ? 1.0 : 0.0)
self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height))
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundNode.frame.minX, y: 0.0), size: CGSize(width:self.extractedBackgroundNode.frame.width, height: size.height))
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -sideInset, bottom: 0.0, right: -sideInset)
@ -140,17 +263,75 @@ private final class ItemNode: ASDisplayNode {
}
func animateBadgeIn() {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
self.badgeContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1)
transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0)
if !self.isReordering {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
self.badgeContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1)
transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0)
}
}
func animateBadgeOut() {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
self.badgeContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0)
transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1)
if !self.isReordering {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
self.badgeContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0)
transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1)
}
}
private func startShaking() {
func degreesToRadians(_ x: CGFloat) -> CGFloat {
return .pi * x / 180.0
}
let duration: Double = 0.4
let displacement: CGFloat = 1.0
let degreesRotation: CGFloat = 2.0
let negativeDisplacement = -1.0 * displacement
let position = CAKeyframeAnimation.init(keyPath: "position")
position.beginTime = 0.8
position.duration = duration
position.values = [
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint(x: 0, y: 0)),
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
]
position.calculationMode = .linear
position.isRemovedOnCompletion = false
position.repeatCount = Float.greatestFiniteMagnitude
position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
position.isAdditive = true
let transform = CAKeyframeAnimation.init(keyPath: "transform")
transform.beginTime = 2.6
transform.duration = 0.3
transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
transform.values = [
degreesToRadians(-1.0 * degreesRotation),
degreesToRadians(degreesRotation),
degreesToRadians(-1.0 * degreesRotation)
]
transform.calculationMode = .linear
transform.isRemovedOnCompletion = false
transform.repeatCount = Float.greatestFiniteMagnitude
transform.isAdditive = true
transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
self.layer.add(position, forKey: "shaking_position")
self.layer.add(transform, forKey: "shaking_rotation")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let deleteButtonNode = self.deleteButtonNode {
if deleteButtonNode.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) {
return deleteButtonNode.view
}
}
return super.hitTest(point, with: event)
}
}
@ -180,39 +361,13 @@ enum ChatListFilterTabEntry: Equatable {
return filter.text
}
}
}
private final class AddItemNode: HighlightableButtonNode {
private let iconNode: ASImageNode
var pressed: (() -> Void)?
private var theme: PresentationTheme?
override init() {
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
super.init()
self.addSubnode(self.iconNode)
self.addTarget(self, action: #selector(self.onPressed), forControlEvents: .touchUpInside)
}
@objc private func onPressed() {
self.pressed?()
}
func update(size: CGSize, theme: PresentationTheme) {
if self.theme !== theme {
self.theme = theme
self.iconNode.image = PresentationResourcesItemList.plusIconImage(theme)
}
if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
func shortTitle(strings: PresentationStrings) -> String {
switch self {
case .all:
return "All"
case let .filter(filter):
return filter.text
}
}
}
@ -221,13 +376,32 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
private let scrollNode: ASScrollNode
private let selectedLineNode: ASImageNode
private var itemNodes: [ChatListFilterTabEntryId: ItemNode] = [:]
private let addNode: AddItemNode
var tabSelected: ((ChatListFilterTabEntryId) -> Void)?
var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)?
var addFilter: (() -> Void)?
var contextGesture: ((Int32, ContextExtractedContentContainingNode, ContextGesture) -> Void)?
private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, presentationData: PresentationData)?
private var reorderingGesture: ReorderingGestureRecognizer?
private var reorderingItem: ChatListFilterTabEntryId?
private var reorderingItemPosition: (initial: CGFloat, offset: CGFloat)?
private var reorderingAutoScrollAnimator: ConstantDisplayLinkAnimator?
private var reorderedItemIds: [ChatListFilterTabEntryId]?
private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData)?
var reorderedFilterIds: [Int32]? {
return self.reorderedItemIds.flatMap {
$0.compactMap {
switch $0 {
case .all:
return nil
case let .filter(id):
return id
}
}
}
}
override init() {
self.scrollNode = ASScrollNode()
@ -236,8 +410,6 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
self.selectedLineNode.displaysAsynchronously = false
self.selectedLineNode.displayWithoutProcessing = true
self.addNode = AddItemNode()
super.init()
self.scrollNode.view.showsHorizontalScrollIndicator = false
@ -250,11 +422,118 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.selectedLineNode)
self.scrollNode.addSubnode(self.addNode)
self.addNode.pressed = { [weak self] in
self?.addFilter?()
}
let reorderingGesture = ReorderingGestureRecognizer(shouldBegin: { [weak self] point in
guard let strongSelf = self else {
return false
}
for (id, itemNode) in strongSelf.itemNodes {
if itemNode.view.convert(itemNode.bounds, to: strongSelf.view).contains(point) {
if case .all = id {
return false
}
return true
}
}
return false
}, began: { [weak self] point in
guard let strongSelf = self, let _ = strongSelf.currentParams else {
return
}
for (id, itemNode) in strongSelf.itemNodes {
let itemFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.view)
if itemFrame.contains(point) {
strongSelf.reorderingItem = id
itemNode.frame = itemFrame
strongSelf.reorderingAutoScrollAnimator = ConstantDisplayLinkAnimator(update: {
guard let strongSelf = self, let currentLocation = strongSelf.reorderingGesture?.currentLocation else {
return
}
let edgeWidth: CGFloat = 20.0
if currentLocation.x <= edgeWidth {
var contentOffset = strongSelf.scrollNode.view.contentOffset
contentOffset.x = max(0.0, contentOffset.x - 3.0)
strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false)
} else if currentLocation.x >= strongSelf.bounds.width - edgeWidth {
var contentOffset = strongSelf.scrollNode.view.contentOffset
contentOffset.x = max(0.0, min(strongSelf.scrollNode.view.contentSize.width - strongSelf.scrollNode.bounds.width, contentOffset.x + 3.0))
strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false)
}
})
strongSelf.reorderingAutoScrollAnimator?.isPaused = false
strongSelf.addSubnode(itemNode)
strongSelf.reorderingItemPosition = (itemNode.frame.minX, 0.0)
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
}
return
}
}
}, ended: { [weak self] in
guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else {
return
}
if let itemNode = strongSelf.itemNodes[reorderingItem] {
let projectedItemFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.scrollNode.view)
itemNode.frame = projectedItemFrame
strongSelf.scrollNode.addSubnode(itemNode)
}
strongSelf.reorderingItem = nil
strongSelf.reorderingItemPosition = nil
strongSelf.reorderingAutoScrollAnimator?.invalidate()
strongSelf.reorderingAutoScrollAnimator = nil
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
}
}, moved: { [weak self] offset in
guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else {
return
}
if let reorderingItemNode = strongSelf.itemNodes[reorderingItem], let (initial, _) = strongSelf.reorderingItemPosition, let reorderedItemIds = strongSelf.reorderedItemIds, let currentItemIndex = reorderedItemIds.firstIndex(of: reorderingItem) {
for (id, itemNode) in strongSelf.itemNodes {
guard let itemIndex = reorderedItemIds.firstIndex(of: id) else {
continue
}
if id != reorderingItem {
let itemFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.view)
if reorderingItemNode.frame.intersects(itemFrame) {
let targetIndex: Int
if reorderingItemNode.frame.midX < itemFrame.midX {
targetIndex = max(1, itemIndex - 1)
} else {
targetIndex = max(1, min(reorderedItemIds.count - 1, itemIndex))
}
if targetIndex != currentItemIndex {
var updatedReorderedItemIds = reorderedItemIds
if targetIndex > currentItemIndex {
updatedReorderedItemIds.insert(reorderingItem, at: targetIndex + 1)
updatedReorderedItemIds.remove(at: currentItemIndex)
} else {
updatedReorderedItemIds.remove(at: currentItemIndex)
updatedReorderedItemIds.insert(reorderingItem, at: targetIndex)
}
strongSelf.reorderedItemIds = updatedReorderedItemIds
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
}
}
break
}
}
}
strongSelf.reorderingItemPosition = (initial, offset)
}
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .immediate)
}
})
self.reorderingGesture = reorderingGesture
self.view.addGestureRecognizer(reorderingGesture)
reorderingGesture.isEnabled = false
}
private var previousSelectedAbsFrame: CGRect?
@ -265,9 +544,10 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
self.scrollNode.layer.removeAllAnimations()
}
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, transitionFraction: CGFloat, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter
let previousScrollBounds = self.scrollNode.bounds
let previousContentWidth = self.scrollNode.view.contentSize.width
if self.currentParams?.presentationData.theme !== presentationData.theme {
self.selectedLineNode.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in
@ -277,7 +557,30 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
})?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 1)
}
self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, presentationData: presentationData)
if isReordering {
if let reorderedItemIds = self.reorderedItemIds {
let currentIds = Set(reorderedItemIds)
if currentIds != Set(filters.map { $0.id }) {
var updatedReorderedItemIds = reorderedItemIds.filter { id in
return filters.contains(where: { $0.id == id })
}
for filter in filters {
if !currentIds.contains(filter.id) {
updatedReorderedItemIds.append(filter.id)
}
}
self.reorderedItemIds = updatedReorderedItemIds
}
} else {
self.reorderedItemIds = filters.map { $0.id }
}
} else if self.reorderedItemIds != nil {
self.reorderedItemIds = nil
}
self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering, isEditing, transitionFraction, presentationData: presentationData)
self.reorderingGesture?.isEnabled = isEditing || isReordering
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
@ -288,7 +591,18 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
var badgeAnimations: [ChatListFilterTabEntryId: BadgeAnimation] = [:]
for filter in filters {
var reorderedFilters: [ChatListFilterTabEntry] = filters
if let reorderedItemIds = self.reorderedItemIds {
reorderedFilters = reorderedItemIds.compactMap { id -> ChatListFilterTabEntry? in
if let index = filters.firstIndex(where: { $0.id == id }) {
return filters[index]
} else {
return nil
}
}
}
for filter in reorderedFilters {
let itemNode: ItemNode
var wasAdded = false
if let current = self.itemNodes[filter.id] {
@ -297,6 +611,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
wasAdded = true
itemNode = ItemNode(pressed: { [weak self] in
self?.tabSelected?(filter.id)
}, requestedDeletion: { [weak self] in
self?.tabRequestedDeletion?(filter.id)
}, contextGesture: { [weak self] sourceNode, gesture in
guard let strongSelf = self else {
return
@ -325,7 +641,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
if !wasAdded && (itemNode.unreadCount != 0) != (unreadCount != 0) {
badgeAnimations[filter.id] = (unreadCount != 0) ? .in : .out
}
itemNode.updateText(title: filter.title(strings: presentationData.strings), unreadCount: unreadCount, isNoFilter: isNoFilter, isSelected: selectedFilter == filter.id, presentationData: presentationData)
itemNode.updateText(title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, isNoFilter: isNoFilter, isSelected: selectedFilter == filter.id, isEditing: false, isAllChats: isNoFilter, isReordering: isEditing || isReordering, presentationData: presentationData, transition: transition)
}
var removeKeys: [ChatListFilterTabEntryId] = []
for (id, _) in self.itemNodes {
@ -335,15 +651,18 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
}
for id in removeKeys {
if let itemNode = self.itemNodes.removeValue(forKey: id) {
itemNode.removeFromSupernode()
transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
transition.updateTransformScale(node: itemNode, scale: 0.1)
}
}
var tabSizes: [(CGSize, ItemNode, Bool)] = []
var tabSizes: [(ChatListFilterTabEntryId, CGSize, CGSize, ItemNode, Bool)] = []
var totalRawTabSize: CGFloat = 0.0
var selectionFrames: [CGRect] = []
for filter in filters {
for filter in reorderedFilters {
guard let itemNode = self.itemNodes[filter.id] else {
continue
}
@ -351,9 +670,10 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
if wasAdded {
self.scrollNode.addSubnode(itemNode)
}
let paneNodeWidth = itemNode.updateLayout(height: size.height, transition: transition)
let (paneNodeWidth, paneNodeShortWidth) = itemNode.updateLayout(height: size.height, transition: transition)
let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height)
tabSizes.append((paneNodeSize, itemNode, wasAdded))
let paneNodeShortSize = CGSize(width: paneNodeShortWidth, height: size.height)
tabSizes.append((filter.id, paneNodeSize, paneNodeShortSize, itemNode, wasAdded))
totalRawTabSize += paneNodeSize.width
if case .animated = transition, let badgeAnimation = badgeAnimations[filter.id] {
@ -370,35 +690,56 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
let sideInset: CGFloat = 16.0
var leftOffset: CGFloat = sideInset
var longTitlesWidth: CGFloat = sideInset
for i in 0 ..< tabSizes.count {
let (paneNodeSize, paneNode, wasAdded) = tabSizes[i]
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize)
if wasAdded {
paneNode.frame = paneFrame
paneNode.alpha = 0.0
transition.updateAlpha(node: paneNode, alpha: 1.0)
} else {
transition.updateFrameAdditive(node: paneNode, frame: paneFrame)
let (itemId, paneNodeSize, paneNodeShortSize, paneNode, wasAdded) = tabSizes[i]
longTitlesWidth += paneNodeSize.width
if i != tabSizes.count - 1 {
longTitlesWidth += minSpacing
}
paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0)
}
longTitlesWidth += sideInset
let useShortTitles = longTitlesWidth > size.width
for i in 0 ..< tabSizes.count {
let (itemId, paneNodeLongSize, paneNodeShortSize, paneNode, wasAdded) = tabSizes[i]
let useShortTitle = itemId == .all && useShortTitles
let paneNodeSize = useShortTitle ? paneNodeShortSize : paneNodeLongSize
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize)
if itemId == self.reorderingItem, let (initial, offset) = self.reorderingItemPosition {
transition.updateSublayerTransformScale(node: paneNode, scale: 1.2)
transition.updateAlpha(node: paneNode, alpha: 0.9)
transition.updateFrameAdditive(node: paneNode, frame: CGRect(origin: CGPoint(x: initial + offset, y: paneFrame.minY), size: paneFrame.size))
} else {
transition.updateSublayerTransformScale(node: paneNode, scale: 1.0)
transition.updateAlpha(node: paneNode, alpha: 1.0)
if wasAdded {
paneNode.frame = paneFrame
paneNode.alpha = 0.0
transition.updateAlpha(node: paneNode, alpha: 1.0)
} else {
transition.updateFrameAdditive(node: paneNode, frame: paneFrame)
}
}
paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0, useShortTitle: useShortTitle, transition: transition)
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing / 2.0, bottom: 0.0, right: -minSpacing / 2.0)
selectionFrames.append(paneFrame)
leftOffset += paneNodeSize.width + minSpacing
}
leftOffset -= minSpacing
leftOffset += sideInset
let addSize = CGSize(width: 32.0, height: size.height)
transition.updateFrame(node: self.addNode, frame: CGRect(origin: CGPoint(x: max(leftOffset, size.width - sideInset - addSize.width + 6.0), y: 0.0), size: addSize))
self.addNode.update(size: addSize, theme: presentationData.theme)
leftOffset += addSize.width + minSpacing
self.scrollNode.view.contentSize = CGSize(width: leftOffset - minSpacing + sideInset - 5.0, height: size.height)
self.scrollNode.view.contentSize = CGSize(width: leftOffset, height: size.height)
var previousFrame: CGRect?
var nextFrame: CGRect?
var selectedFrame: CGRect?
if let selectedFilter = selectedFilter, let currentIndex = filters.firstIndex(where: { $0.id == selectedFilter }) {
if let selectedFilter = selectedFilter, let currentIndex = reorderedFilters.firstIndex(where: { $0.id == selectedFilter }) {
func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t)))
}
@ -423,28 +764,33 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
if wasAdded {
self.selectedLineNode.frame = lineFrame
self.selectedLineNode.alpha = 0.0
transition.updateAlpha(node: self.selectedLineNode, alpha: 1.0)
} else {
transition.updateFrame(node: self.selectedLineNode, frame: lineFrame)
}
if !transitionFraction.isZero {
transition.updateAlpha(node: self.selectedLineNode, alpha: isReordering && selectedFilter == .all ? 0.5 : 1.0)
//if !transitionFraction.isZero {
if let previousSelectedFrame = self.previousSelectedFrame, abs(previousSelectedFrame.offsetBy(dx: -previousScrollBounds.minX, dy: 0.0).midX - previousScrollBounds.width / 2.0) < 1.0 {
let previousContentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(previousSelectedFrame.midX - self.scrollNode.bounds.width / 2.0)))
let previousContentOffsetX = max(0.0, min(previousContentWidth - self.scrollNode.bounds.width, floor(previousSelectedFrame.midX - self.scrollNode.bounds.width / 2.0)))
if abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 {
focusOnSelectedFilter = true
}
}
}
if focusOnSelectedFilter {
if transitionFraction.isZero && selectedFilter == filters.first?.id {
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size))
} 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))
//}
if focusOnSelectedFilter && self.reorderingItem == nil {
let updatedBounds: CGRect
if transitionFraction.isZero && selectedFilter == reorderedFilters.first?.id {
updatedBounds = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
} else if transitionFraction.isZero && selectedFilter == reorderedFilters.last?.id {
updatedBounds = CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size)
} else {
let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0)))
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size))
updatedBounds = CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)
}
} else if !wasAdded, transitionFraction.isZero, let previousSelectedAbsFrame = self.previousSelectedAbsFrame {
self.scrollNode.bounds = updatedBounds
}
transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX)
/*else if false, !wasAdded, transitionFraction.isZero, let previousSelectedAbsFrame = self.previousSelectedAbsFrame {
let contentOffsetX: CGFloat
if previousScrollBounds.minX.isZero {
contentOffsetX = 0.0
@ -454,7 +800,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, selectedFrame.midX - previousSelectedAbsFrame.midX))
}
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size))
}
}*/
self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0)
self.previousSelectedFrame = selectedFrame
} else {
@ -464,3 +810,131 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
}
}
}
private class ReorderingGestureRecognizerTimerTarget: NSObject {
private let f: () -> Void
init(_ f: @escaping () -> Void) {
self.f = f
super.init()
}
@objc func timerEvent() {
self.f()
}
}
private final class ReorderingGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
private let shouldBegin: (CGPoint) -> Bool
private let began: (CGPoint) -> Void
private let ended: () -> Void
private let moved: (CGFloat) -> Void
private var initialLocation: CGPoint?
private var delayTimer: Foundation.Timer?
var currentLocation: CGPoint?
init(shouldBegin: @escaping (CGPoint) -> Bool, began: @escaping (CGPoint) -> Void, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) {
self.shouldBegin = shouldBegin
self.began = began
self.ended = ended
self.moved = moved
super.init(target: nil, action: nil)
self.delegate = self
}
override func reset() {
super.reset()
self.initialLocation = nil
self.delayTimer?.invalidate()
self.delayTimer = nil
self.currentLocation = nil
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIPanGestureRecognizer {
return true
} else {
return false
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
guard let location = touches.first?.location(in: self.view) else {
self.state = .failed
return
}
if self.state == .possible {
if self.delayTimer == nil {
if !self.shouldBegin(location) {
self.state = .failed
return
}
self.initialLocation = location
let timer = Foundation.Timer(timeInterval: 0.2, target: ReorderingGestureRecognizerTimerTarget { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.delayTimer = nil
strongSelf.state = .began
strongSelf.began(location)
}, selector: #selector(ReorderingGestureRecognizerTimerTarget.timerEvent), userInfo: nil, repeats: false)
self.delayTimer = timer
RunLoop.main.add(timer, forMode: .common)
} else {
self.state = .failed
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if self.state == .began || self.state == .changed {
self.delayTimer?.invalidate()
self.ended()
self.state = .failed
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
if self.state == .began || self.state == .changed {
self.delayTimer?.invalidate()
self.ended()
self.state = .failed
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) else {
return
}
let offset = location.x - initialLocation.x
self.currentLocation = location
if self.delayTimer != nil {
if abs(offset) > 4.0 {
self.delayTimer?.invalidate()
self.state = .failed
return
}
} else {
if self.state == .began || self.state == .changed {
self.state = .changed
self.moved(offset)
}
}
}
}

View File

@ -32,7 +32,11 @@ struct ChatListNodeViewUpdate {
func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredicate {
let includePeers = Set(filter.includePeers)
let excludePeers = Set(filter.excludePeers)
return ChatListFilterPredicate(includePeerIds: includePeers, excludePeerIds: excludePeers, include: { peer, notificationSettings, isUnread, isContact, isArchived in
var includeAdditionalPeerGroupIds: [PeerGroupId] = []
if !filter.excludeArchived {
includeAdditionalPeerGroupIds.append(Namespaces.PeerGroup.archive)
}
return ChatListFilterPredicate(includePeerIds: includePeers, excludePeerIds: excludePeers, includeAdditionalPeerGroupIds: includeAdditionalPeerGroupIds, include: { peer, notificationSettings, isUnread, isContact in
if filter.excludeRead {
if !isUnread {
return false
@ -47,11 +51,6 @@ func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredic
return false
}
}
if filter.excludeArchived {
if isArchived {
return false
}
}
if !filter.categories.contains(.contacts) && isContact {
if let user = peer as? TelegramUser {
if user.botInfo == nil {
@ -77,23 +76,12 @@ func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredic
}
}
}
if !filter.categories.contains(.smallGroups) {
if !filter.categories.contains(.groups) {
if let _ = peer as? TelegramGroup {
return false
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
if channel.username == nil {
return false
}
}
}
}
if !filter.categories.contains(.largeGroups) {
if let channel = peer as? TelegramChannel {
if case .group = channel.info {
if channel.username != nil {
return false
}
return false
}
}
}

View File

@ -101,12 +101,9 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
if filter.data.categories.contains(.nonContacts) {
tags.append(.nonContact)
}
if filter.data.categories.contains(.smallGroups) {
if filter.data.categories.contains(.groups) {
tags.append(.smallGroup)
}
if filter.data.categories.contains(.largeGroups) {
tags.append(.largeGroup)
}
if filter.data.categories.contains(.bots) {
tags.append(.bot)
}

View File

@ -465,7 +465,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
case .none:
updatedSelectionNode = nil
case let .selectable(selected):
rightInset += 28.0
rightInset += 38.0
isSelected = selected
let selectionNode: CheckNode

View File

@ -1,7 +1,7 @@
import Foundation
enum ChatListOperation {
case InsertEntry(ChatListIndex, IntermediateMessage?, CombinedPeerReadState?, PeerChatListEmbeddedInterfaceState?)
case InsertEntry(ChatListIndex, MessageIndex?)
case InsertHole(ChatListHole)
case RemoveEntry([ChatListIndex])
case RemoveHoles([ChatListIndex])
@ -22,15 +22,15 @@ enum ChatListEntryInfo {
}
enum ChatListIntermediateEntry {
case message(ChatListIndex, IntermediateMessage?, PeerChatListEmbeddedInterfaceState?)
case message(ChatListIndex, MessageIndex?)
case hole(ChatListHole)
var index: ChatListIndex {
switch self {
case let .message(index, _, _):
return index
case let .hole(hole):
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
case let .message(index, _):
return index
case let .hole(hole):
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
}
}
}
@ -78,12 +78,12 @@ private func extractKey(_ key: ValueBoxKey) -> (groupId: PeerGroupId, pinningInd
)
}
private func readEntry(groupId: PeerGroupId, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, key: ValueBoxKey, value: ReadBuffer) -> ChatListIntermediateEntry {
private func readEntry(groupId: PeerGroupId, key: ValueBoxKey, value: ReadBuffer) -> ChatListIntermediateEntry {
let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key)
assert(groupId == keyGroupId)
let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex)
if type == ChatListEntryType.message.rawValue {
var message: IntermediateMessage?
var messageIndex: MessageIndex?
if value.length != 0 {
var idNamespace: Int32 = 0
value.read(&idNamespace, offset: 0, length: 4)
@ -92,9 +92,9 @@ private func readEntry(groupId: PeerGroupId, messageHistoryTable: MessageHistory
var indexTimestamp: Int32 = 0
value.read(&indexTimestamp, offset: 0, length: 4)
message = messageHistoryTable.getMessage(MessageIndex(id: MessageId(peerId: index.messageIndex.id.peerId, namespace: idNamespace, id: idId), timestamp: indexTimestamp))
messageIndex = MessageIndex(id: MessageId(peerId: index.messageIndex.id.peerId, namespace: idNamespace, id: idId), timestamp: indexTimestamp)
}
return .message(index, message, peerChatInterfaceStateTable.get(index.messageIndex.id.peerId)?.chatListEmbeddedState)
return .message(index, messageIndex)
} else if type == ChatListEntryType.hole.rawValue {
return .hole(ChatListHole(index: index.messageIndex))
} else {
@ -203,9 +203,9 @@ final class ChatListTable: Table {
var itemIds: [(id: PinnedItemId, rank: Int)] = []
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.key(groupId: groupId, index: ChatListIndex(pinningIndex: UInt16.max - 1, messageIndex: MessageIndex.absoluteUpperBound()), type: .message).successor, values: { key, value in
let keyIndex = extractKey(key)
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
let entry = readEntry(groupId: groupId, key: key, value: value)
switch entry {
case let .message(index, _, _):
case let .message(index, _):
itemIds.append((.peer(index.messageIndex.id.peerId), Int(keyIndex.pinningIndex ?? 0)))
default:
break
@ -256,7 +256,7 @@ final class ChatListTable: Table {
if let peer = postbox.peerTable.get(messageIndex.id.peerId) {
let isUnread = postbox.readStateTable.getCombinedState(messageIndex.id.peerId)?.isUnread ?? false
let isContact = postbox.contactsTable.isContact(peerId: messageIndex.id.peerId)
if filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(messageIndex.id.peerId), isUnread: isUnread, isContact: isContact, isArchived: groupId != .root) {
if filterPredicate.includes(peer: peer, groupId: groupId, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(messageIndex.id.peerId), isUnread: isUnread, isContact: isContact) {
passFilter = true
} else {
passFilter = false
@ -306,7 +306,7 @@ final class ChatListTable: Table {
self.ensureInitialized(groupId: groupId)
}
let topMessage = messageHistoryTable.topMessage(peerId)
let topMessage = messageHistoryTable.topIndex(peerId: peerId)
let embeddedChatState = peerChatInterfaceStateTable.get(peerId)?.chatListEmbeddedState
let rawTopMessageIndex: MessageIndex?
@ -339,7 +339,7 @@ final class ChatListTable: Table {
addOperation(.RemoveEntry([currentOrderingIndex]), groupId: currentGroupId, to: &operations)
}
self.justInsertIndex(groupId: updatedGroupId, index: updatedOrderingIndex, topMessageIndex: rawTopMessageIndex)
addOperation(.InsertEntry(updatedOrderingIndex, topMessage, messageHistoryTable.readStateTable.getCombinedState(peerId), embeddedChatState), groupId: updatedGroupId, to: &operations)
addOperation(.InsertEntry(updatedOrderingIndex, topMessage), groupId: updatedGroupId, to: &operations)
} else {
if let (currentGroupId, currentOrderingIndex) = currentGroupAndIndex {
self.justRemoveMessageIndex(groupId: currentGroupId, index: currentOrderingIndex)
@ -410,7 +410,7 @@ final class ChatListTable: Table {
var upper: ChatListIntermediateEntry?
self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: index, type: .message), end: self.lowerBound(groupId: groupId), values: { key, value in
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
let entry = readEntry(groupId: groupId, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
lowerEntries.append(entry)
@ -429,7 +429,7 @@ final class ChatListTable: Table {
}
self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: index, type: .message).predecessor, end: self.upperBound(groupId: groupId), values: { key, value in
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
let entry = readEntry(groupId: groupId, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
upperEntries.append(entry)
@ -519,6 +519,31 @@ final class ChatListTable: Table {
}
return result
}
func entries(groupId: PeerGroupId, from fromIndex: (ChatListIndex, Bool), to toIndex: (ChatListIndex, Bool), peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> [ChatListIntermediateEntry] {
self.ensureInitialized(groupId: groupId)
var entries: [ChatListIntermediateEntry] = []
let fromKey = self.key(groupId: groupId, index: fromIndex.0, type: fromIndex.1 ? .message : .hole)
let toKey = self.key(groupId: groupId, index: toIndex.0, type: toIndex.1 ? .message : .hole)
self.valueBox.filteredRange(self.table, start: fromKey, end: toKey, values: { key, value in
let entry = readEntry(groupId: groupId, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
entries.append(entry)
return .accept
} else {
return .skip
}
} else {
entries.append(entry)
return .accept
}
}, limit: count)
assert(entries.count <= count)
return entries
}
func earlierEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> [ChatListIntermediateEntry] {
self.ensureInitialized(groupId: groupId)
@ -532,7 +557,7 @@ final class ChatListTable: Table {
}
self.valueBox.filteredRange(self.table, start: key, end: self.lowerBound(groupId: groupId), values: { key, value in
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
let entry = readEntry(groupId: groupId, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
entries.append(entry)
@ -596,7 +621,7 @@ final class ChatListTable: Table {
}
self.valueBox.filteredRange(self.table, start: key, end: self.upperBound(groupId: groupId), values: { key, value in
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
let entry = readEntry(groupId: groupId, key: key, value: value)
if let predicate = predicate {
if predicate(entry) {
entries.append(entry)
@ -640,9 +665,7 @@ final class ChatListTable: Table {
break
}
if let topMessageIndex = index.topMessageIndex {
if let message = messageHistoryTable.getMessage(topMessageIndex) {
return ChatListIntermediateEntry.message(ChatListIndex(pinningIndex: nil, messageIndex: topMessageIndex), message, nil)
}
return ChatListIntermediateEntry.message(ChatListIndex(pinningIndex: nil, messageIndex: topMessageIndex), topMessageIndex)
}
return nil
}
@ -651,7 +674,7 @@ final class ChatListTable: Table {
if let (peerGroupId, index) = self.getPeerChatListIndex(peerId: peerId), peerGroupId == groupId {
let key = self.key(groupId: groupId, index: index, type: .message)
if let value = self.valueBox.get(self.table, key: key) {
return readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
return readEntry(groupId: groupId, key: key, value: value)
} else {
return nil
}
@ -664,7 +687,7 @@ final class ChatListTable: Table {
if let (peerGroupId, index) = self.getPeerChatListIndex(peerId: peerId) {
let key = self.key(groupId: peerGroupId, index: index, type: .message)
if let value = self.valueBox.get(self.table, key: key) {
return readEntry(groupId: peerGroupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
return readEntry(groupId: peerGroupId, key: key, value: value)
} else {
return nil
}
@ -705,10 +728,7 @@ final class ChatListTable: Table {
func allPeerIds(groupId: PeerGroupId) -> [PeerId] {
var peerIds: [PeerId] = []
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), keys: { key in
let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key)
assert(groupId == keyGroupId)
let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex)
let (_, _, messageIndex, type) = extractKey(key)
if type == ChatListEntryType.message.rawValue {
peerIds.append(messageIndex.id.peerId)
}

View File

@ -165,9 +165,9 @@ public enum ChatListEntry: Comparable {
}
}
private func processedChatListEntry(_ entry: MutableChatListEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) -> MutableChatListEntry {
/*private func processedChatListEntry(_ entry: MutableChatListEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) -> MutableChatListEntry {
switch entry {
case let .IntermediateMessageEntry(index, message, readState, embeddedState):
case let .IntermediateMessageEntry(index, messageIndex):
var updatedMessage = message
if let message = message, let cachedData = cachedDataTable.get(message.id.peerId), let associatedHistoryMessageId = cachedData.associatedHistoryMessageId, message.id.id == 1 {
if let messageIndex = messageHistoryTable.messageHistoryIndexTable.earlierEntries(id: associatedHistoryMessageId, count: 1).first {
@ -180,17 +180,17 @@ private func processedChatListEntry(_ entry: MutableChatListEntry, cachedDataTab
default:
return entry
}
}
}*/
enum MutableChatListEntry: Equatable {
case IntermediateMessageEntry(ChatListIndex, IntermediateMessage?, CombinedPeerReadState?, PeerChatListEmbeddedInterfaceState?)
case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo, Bool, Bool)
case IntermediateMessageEntry(index: ChatListIndex, messageIndex: MessageIndex?)
case MessageEntry(index: ChatListIndex, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, tagSummaryInfo: ChatListMessageTagSummaryInfo, hasFailedMessages: Bool, isContact: Bool)
case HoleEntry(ChatListHole)
init(_ intermediateEntry: ChatListIntermediateEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) {
switch intermediateEntry {
case let .message(index, message, embeddedState):
self = processedChatListEntry(.IntermediateMessageEntry(index, message, readStateTable.getCombinedState(index.messageIndex.id.peerId), embeddedState), cachedDataTable: cachedDataTable, readStateTable: readStateTable, messageHistoryTable: messageHistoryTable)
case let .message(index, messageIndex):
self = .IntermediateMessageEntry(index: index, messageIndex: messageIndex)
case let .hole(hole):
self = .HoleEntry(hole)
}
@ -198,8 +198,8 @@ enum MutableChatListEntry: Equatable {
var index: ChatListIndex {
switch self {
case let .IntermediateMessageEntry(index, _, _, _):
return index
case let .IntermediateMessageEntry(intermediateMessageEntry):
return intermediateMessageEntry.index
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _):
return index
case let .HoleEntry(hole):
@ -254,69 +254,32 @@ private enum ChatListEntryType {
case groupReference
}
private func updateMessagePeers(_ message: Message, updatedPeers: [PeerId: Peer]) -> Message? {
var updated = false
for (peerId, currentPeer) in message.peers {
if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) {
updated = true
break
}
}
if updated {
var peers = SimpleDictionary<PeerId, Peer>()
for (peerId, currentPeer) in message.peers {
if let updatedPeer = updatedPeers[peerId] {
peers[peerId] = updatedPeer
} else {
peers[peerId] = currentPeer
}
}
return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds)
}
return nil
}
private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [PeerId: Peer]) -> RenderedPeer? {
var updated = false
for (peerId, currentPeer) in renderedPeer.peers {
if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) {
updated = true
break
}
}
if updated {
var peers = SimpleDictionary<PeerId, Peer>()
for (peerId, currentPeer) in renderedPeer.peers {
if let updatedPeer = updatedPeers[peerId] {
peers[peerId] = updatedPeer
} else {
peers[peerId] = currentPeer
}
}
return RenderedPeer(peerId: renderedPeer.peerId, peers: peers)
}
return nil
}
public struct ChatListFilterPredicate {
public var includePeerIds: Set<PeerId>
public var excludePeerIds: Set<PeerId>
public var include: (Peer, PeerNotificationSettings?, Bool, Bool, Bool) -> Bool
public var includeAdditionalPeerGroupIds: [PeerGroupId]
public var include: (Peer, PeerNotificationSettings?, Bool, Bool) -> Bool
public init(includePeerIds: Set<PeerId>, excludePeerIds: Set<PeerId>, include: @escaping (Peer, PeerNotificationSettings?, Bool, Bool, Bool) -> Bool) {
public init(includePeerIds: Set<PeerId>, excludePeerIds: Set<PeerId>, includeAdditionalPeerGroupIds: [PeerGroupId], include: @escaping (Peer, PeerNotificationSettings?, Bool, Bool) -> Bool) {
self.includePeerIds = includePeerIds
self.excludePeerIds = excludePeerIds
self.includeAdditionalPeerGroupIds = includeAdditionalPeerGroupIds
self.include = include
}
func includes(peer: Peer, notificationSettings: PeerNotificationSettings?, isUnread: Bool, isContact: Bool, isArchived: Bool) -> Bool {
func includes(peer: Peer, groupId: PeerGroupId, notificationSettings: PeerNotificationSettings?, isUnread: Bool, isContact: Bool) -> Bool {
if self.excludePeerIds.contains(peer.id) {
return false
}
if self.includePeerIds.contains(peer.id) {
return true
}
return self.include(peer, notificationSettings, isUnread, isContact, isArchived)
if groupId != .root {
if !self.includeAdditionalPeerGroupIds.contains(groupId) {
return false
}
}
return self.include(peer, notificationSettings, isUnread, isContact)
}
}
@ -324,64 +287,48 @@ final class MutableChatListView {
let groupId: PeerGroupId
let filterPredicate: ChatListFilterPredicate?
private let summaryComponents: ChatListEntrySummaryComponents
fileprivate var additionalItemIds: Set<PeerId>
fileprivate var additionalItemEntries: [MutableChatListEntry]
fileprivate var additionalMixedItemIds: Set<PeerId>
fileprivate var additionalMixedPinnedItemIds: Set<PeerId>
fileprivate var additionalMixedItemEntries: [MutableChatListEntry]
fileprivate var additionalMixedPinnedEntries: [MutableChatListEntry]
fileprivate var earlier: MutableChatListEntry?
fileprivate var later: MutableChatListEntry?
fileprivate var entries: [MutableChatListEntry]
fileprivate var groupEntries: [ChatListGroupReferenceEntry]
private var count: Int
private let spaces: [ChatListViewSpace]
fileprivate var state: ChatListViewState
fileprivate var sampledState: ChatListViewSample
init(postbox: Postbox, groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate?, aroundIndex: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents) {
let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: groupId, index: aroundIndex, count: count, filterPredicate: filterPredicate)
self.groupId = groupId
self.filterPredicate = filterPredicate
self.earlier = earlier
self.entries = entries
self.later = later
self.count = count
self.summaryComponents = summaryComponents
self.additionalItemEntries = []
self.additionalMixedItemEntries = []
self.additionalMixedPinnedEntries = []
self.additionalMixedItemIds = Set()
self.additionalMixedPinnedItemIds = Set()
var spaces: [ChatListViewSpace] = [
.group(groupId: self.groupId, pinned: .notPinned)
]
if let filterPredicate = self.filterPredicate {
self.additionalMixedItemIds.formUnion(filterPredicate.includePeerIds)
for (itemId, _) in postbox.chatListTable.getPinnedItemIds(groupId: self.groupId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
switch itemId {
case let .peer(peerId):
self.additionalMixedPinnedItemIds.insert(peerId)
}
}
}
for peerId in self.additionalMixedItemIds {
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))
}
}
for peerId in self.additionalMixedPinnedItemIds {
if let entry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
self.additionalMixedPinnedEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
spaces.append(.group(groupId: self.groupId, pinned: .includePinnedAsUnpinned))
for additionalGroupId in filterPredicate.includeAdditionalPeerGroupIds {
spaces.append(.group(groupId: additionalGroupId, pinned: .notPinned))
spaces.append(.group(groupId: additionalGroupId, pinned: .includePinnedAsUnpinned))
}
} else {
spaces.append(.group(groupId: self.groupId, pinned: .includePinned))
}
self.spaces = spaces
self.state = ChatListViewState(postbox: postbox, spaces: self.spaces, anchorIndex: aroundIndex, filterPredicate: self.filterPredicate, summaryComponents: self.summaryComponents, halfLimit: count)
self.sampledState = self.state.sample(postbox: postbox)
self.count = count
if case .root = groupId, self.filterPredicate == nil {
let itemIds = postbox.additionalChatListItemsTable.get()
/*let itemIds = postbox.additionalChatListItemsTable.get()
self.additionalItemIds = Set(itemIds)
for peerId in itemIds {
if let entry = postbox.chatListTable.getStandalone(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable) {
self.additionalItemEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
}
}
}*/
self.groupEntries = []
self.reloadGroups(postbox: postbox)
} else {
self.additionalItemIds = Set()
//self.additionalItemIds = Set()
self.groupEntries = []
}
}
@ -456,87 +403,31 @@ final class MutableChatListView {
}
func refreshDueToExternalTransaction(postbox: Postbox) -> Bool {
var index = ChatListIndex.absoluteUpperBound
if !self.entries.isEmpty && self.later != nil {
index = self.entries[self.entries.count / 2].index
}
var updated = false
self.state = ChatListViewState(postbox: postbox, spaces: self.spaces, anchorIndex: .absoluteUpperBound, filterPredicate: self.filterPredicate, summaryComponents: self.summaryComponents, halfLimit: self.count)
self.sampledState = self.state.sample(postbox: postbox)
updated = true
let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.count, filterPredicate: self.filterPredicate)
let currentGroupEntries = self.groupEntries
self.reloadGroups(postbox: postbox)
var updated = false
if self.groupEntries != currentGroupEntries {
updated = true
}
if entries != self.entries || earlier != self.earlier || later != self.later {
self.entries = entries
self.earlier = earlier
self.later = later
updated = true
}
return updated
}
func replay(postbox: Postbox, operations: [PeerGroupId: [ChatListOperation]], updatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence], transaction: PostboxTransaction, context: MutableChatListViewReplayContext) -> Bool {
var hasChanges = false
if let groupOperations = operations[self.groupId] {
for operation in groupOperations {
switch operation {
case let .InsertEntry(index, message, combinedReadState, embeddedState):
if self.add(.IntermediateMessageEntry(index, message, combinedReadState, embeddedState), postbox: postbox) {
hasChanges = true
}
case let .InsertHole(index):
if self.add(.HoleEntry(index), postbox: postbox) {
hasChanges = true
}
case let .RemoveEntry(indices):
if self.remove(Set(indices), type: .message, context: context) {
hasChanges = true
}
case let .RemoveHoles(indices):
if self.remove(Set(indices), type: .hole, context: context) {
hasChanges = true
}
}
}
if self.state.replay(postbox: postbox, transaction: transaction) {
self.sampledState = self.state.sample(postbox: postbox)
hasChanges = true
}
/*if let filterPredicate = self.filterPredicate, !filterPredicate.includePeerIds.isEmpty {
for (groupId, groupOperations) in operations {
if groupId == self.groupId {
continue
}
for operation in groupOperations {
switch operation {
case let .InsertEntry(index, message, combinedReadState, embeddedState):
if filterPredicate.includePeerIds.contains(index.messageIndex.id.peerId) {
if self.add(.IntermediateMessageEntry(index, message, combinedReadState, embeddedState), postbox: postbox) {
hasChanges = true
}
}
case .InsertHole:
break
case let .RemoveEntry(indices):
let updatedIndices = indices.filter { index in
return filterPredicate.includePeerIds.contains(index.messageIndex.id.peerId)
}
if !updatedIndices.isEmpty && self.remove(Set(updatedIndices), type: .message, context: context) {
hasChanges = true
}
case .RemoveHoles:
break
}
}
}
}*/
if case .root = self.groupId, self.filterPredicate == nil {
var invalidatedGroups = false
for (groupId, groupOperations) in operations {
@ -573,184 +464,9 @@ final class MutableChatListView {
}
}
if !updatedPeerNotificationSettings.isEmpty {
if let filterPredicate = self.filterPredicate {
for (peerId, settingsChange) in updatedPeerNotificationSettings {
if let peer = postbox.peerTable.get(peerId), !self.additionalMixedItemIds.contains(peerId), !self.additionalMixedPinnedItemIds.contains(peerId) {
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let wasIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.0, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId), isArchived: false)
let isIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.1, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId), isArchived: false)
if wasIncluded != isIncluded {
if isIncluded {
let tableEntry: ChatListIntermediateEntry?
if filterPredicate.includePeerIds.contains(peerId) {
tableEntry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable)
} else {
tableEntry = postbox.chatListTable.getEntry(groupId: self.groupId, peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable)
}
if let entry = tableEntry {
switch entry {
case let .message(index, message, embeddedState):
let combinedReadState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)
if self.add(.IntermediateMessageEntry(entry.index, message, combinedReadState, embeddedState), postbox: postbox) {
hasChanges = true
}
default:
break
}
}
} else {
loop: for i in 0 ..< self.entries.count {
switch self.entries[i] {
case .MessageEntry(let index, _, _, _, _, _, _, _, _, _), .IntermediateMessageEntry(let index, _, _, _):
if index.messageIndex.id.peerId == peerId {
self.entries.remove(at: i)
hasChanges = true
break loop
}
default:
break
}
}
}
}
}
}
}
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
var notificationSettingsPeerId = peer.peerId
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
notificationSettingsPeerId = associatedPeerId
}
if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] {
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
continue
}
}
for i in 0 ..< self.additionalMixedItemEntries.count {
switch self.additionalMixedItemEntries[i] {
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
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, isContact)
hasChanges = true
}
default:
continue
}
}
for i in 0 ..< self.additionalMixedPinnedEntries.count {
switch self.additionalMixedPinnedEntries[i] {
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
var notificationSettingsPeerId = peer.peerId
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
notificationSettingsPeerId = associatedPeerId
}
if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] {
self.additionalMixedPinnedEntries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
continue
}
}
}
/*
if !transaction.updatedFailedMessagePeerIds.isEmpty {
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, previousHasFailed, isContact):
if transaction.updatedFailedMessagePeerIds.contains(index.messageIndex.id.peerId) {
let hasFailed = postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId)
if previousHasFailed != hasFailed {
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
}
default:
continue
}
}
}
if !updatedPeers.isEmpty {
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
var updatedMessage: Message?
if let message = message {
updatedMessage = updateMessagePeers(message, updatedPeers: updatedPeers)
}
let updatedPeer = updatedRenderedPeer(peer, updatedPeers: updatedPeers)
if updatedMessage != nil || updatedPeer != nil {
self.entries[i] = .MessageEntry(index, updatedMessage ?? message, readState, settings, embeddedState, updatedPeer ?? peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
continue
}
}
}
if !updatedPeerPresences.isEmpty {
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, _, summaryInfo, hasFailed, isContact):
var presencePeerId = peer.peerId
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
presencePeerId = associatedPeerId
}
if let presence = updatedPeerPresences[presencePeerId] {
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, presence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
continue
}
}
}
if !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageActionsSummaries.isEmpty {
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, currentSummary, hasFailed, isContact):
var updatedTagSummaryCount: Int32?
var updatedActionsSummaryCount: Int32?
if let tagSummary = self.summaryComponents.tagSummary {
let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: index.messageIndex.id.peerId, namespace: tagSummary.namespace)
if let summary = transaction.currentUpdatedMessageTagSummaries[key] {
updatedTagSummaryCount = summary.count
}
}
if let actionsSummary = self.summaryComponents.actionsSummary {
let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: index.messageIndex.id.peerId, namespace: actionsSummary.namespace)
if let count = transaction.currentUpdatedMessageActionsSummaries[key] {
updatedActionsSummaryCount = count
}
}
if updatedTagSummaryCount != nil || updatedActionsSummaryCount != nil {
let summaryInfo = ChatListMessageTagSummaryInfo(tagSummaryCount: updatedTagSummaryCount ?? currentSummary.tagSummaryCount, actionsSummaryCount: updatedActionsSummaryCount ?? currentSummary.actionsSummaryCount)
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
continue
}
}
}
var updateAdditionalItems = false
if let itemIds = transaction.replacedAdditionalChatListItems {
self.additionalItemIds = Set(itemIds)
@ -803,228 +519,24 @@ final class MutableChatListView {
}
hasChanges = true
}
return hasChanges
}
func add(_ initialEntry: MutableChatListEntry, postbox: Postbox) -> Bool {
if let filterPredicate = self.filterPredicate {
switch initialEntry {
case .IntermediateMessageEntry(let index, _, _, _), .MessageEntry(let index, _, _, _, _, _, _, _, _, _):
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
let isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
let isContact = postbox.contactsTable.isContact(peerId: peer.id)
if !filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread: isUnread, isContact: isContact, isArchived: false) {
return false
}
if self.additionalMixedItemIds.contains(peer.id) {
return false
}
if self.additionalMixedPinnedItemIds.contains(peer.id) {
return false
}
} else {
return false
}
break
default:
break
}
}
let entry = processedChatListEntry(initialEntry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable)
if self.entries.count == 0 {
self.entries.append(entry)
return true
} else {
let first = self.entries[self.entries.count - 1]
let last = self.entries[0]
let next = self.later
if entry.index < last.index {
if self.earlier == nil || self.earlier!.index < entry.index {
if self.entries.count < self.count {
self.entries.insert(entry, at: 0)
} else {
self.earlier = entry
}
return true
} else {
return false
}
} else if entry.index > first.index {
if next != nil && entry.index > next!.index {
if self.later == nil || self.later!.index > entry.index {
if self.entries.count < self.count {
self.entries.append(entry)
} else {
self.later = entry
}
return true
} else {
return false
}
} else {
self.entries.append(entry)
if self.entries.count > self.count {
self.earlier = self.entries[0]
self.entries.remove(at: 0)
}
return true
}
} else if entry != last && entry != first {
var i = self.entries.count
while i >= 1 {
if self.entries[i - 1].index < entry.index {
break
}
i -= 1
}
self.entries.insert(entry, at: i)
if self.entries.count > self.count {
self.earlier = self.entries[0]
self.entries.remove(at: 0)
}
return true
} else {
return false
}
}
}
private func remove(_ indices: Set<ChatListIndex>, type: ChatListEntryType, context: MutableChatListViewReplayContext) -> Bool {
var hasChanges = false
if let earlier = self.earlier, indices.contains(earlier.index) {
var match = false
switch earlier {
case .HoleEntry:
match = type == .hole
case .IntermediateMessageEntry, .MessageEntry:
match = type == .message
/*case .IntermediateGroupReferenceEntry, .GroupReferenceEntry:
match = type == .groupReference*/
}
if match {
context.invalidEarlier = true
hasChanges = true
}
}
if let later = self.later, indices.contains(later.index) {
var match = false
switch later {
case .HoleEntry:
match = type == .hole
case .IntermediateMessageEntry, .MessageEntry:
match = type == .message
/*case .IntermediateGroupReferenceEntry, .GroupReferenceEntry:
match = type == .groupReference*/
}
if match {
context.invalidLater = true
hasChanges = true
}
}
if self.entries.count != 0 {
var i = self.entries.count - 1
while i >= 0 {
if indices.contains(self.entries[i].index) {
var match = false
switch self.entries[i] {
case .HoleEntry:
match = type == .hole
case .IntermediateMessageEntry, .MessageEntry:
match = type == .message
/*case .IntermediateGroupReferenceEntry, .GroupReferenceEntry:
match = type == .groupReference*/
}
if match {
self.entries.remove(at: i)
context.removedEntries = true
hasChanges = true
}
}
i -= 1
}
}
}*/
return hasChanges
}
func complete(postbox: Postbox, context: MutableChatListViewReplayContext) {
if context.removedEntries {
var index = ChatListIndex.absoluteUpperBound
if !self.entries.isEmpty && self.later != nil {
index = self.entries[self.entries.count / 2].index
}
let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.count, filterPredicate: self.filterPredicate)
var previousEntryByPeerId: [PeerId: MutableChatListEntry] = [:]
for entry in self.entries {
switch entry {
case let .MessageEntry(messageEntry):
previousEntryByPeerId[messageEntry.0.messageIndex.id.peerId] = entry
default:
break
}
}
self.entries = entries
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(messageEntry):
if let previousEntry = previousEntryByPeerId[messageEntry.0.messageIndex.id.peerId] {
self.entries[i] = previousEntry
}
default:
break
}
}
self.earlier = earlier
self.later = later
} else {
if context.invalidEarlier {
var earlyId: ChatListIndex?
let i = 0
if i < self.entries.count {
earlyId = self.entries[i].index
}
let earlierEntries = postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlyId, count: 1, filterPredicate: self.filterPredicate)
self.earlier = earlierEntries.first
}
if context.invalidLater {
var laterId: ChatListIndex?
let i = self.entries.count - 1
if i >= 0 {
laterId = self.entries[i].index
}
let laterEntries = postbox.fetchLaterChatEntries(groupId: self.groupId, index: laterId, count: 1, filterPredicate: self.filterPredicate)
self.later = laterEntries.first
}
}
}
func firstHole() -> ChatListHole? {
for entry in self.entries {
if case let .HoleEntry(hole) = entry {
return hole
}
}
return nil
return self.sampledState.hole
}
private func renderEntry(_ entry: MutableChatListEntry, postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) -> MutableChatListEntry? {
/*private func renderEntry(_ entry: MutableChatListEntry, postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) -> MutableChatListEntry? {
switch entry {
case let .IntermediateMessageEntry(index, message, combinedReadState, embeddedState):
case let .IntermediateMessageEntry(index, messageIndex):
let renderedMessage: Message?
if let message = message {
renderedMessage = renderMessage(message)
if let messageIndex = messageIndex {
renderedMessage = postbox.messageHistoryTable.getMessage(messageIndex).flatMap(renderMessage)
} else {
renderedMessage = nil
}
@ -1063,14 +575,14 @@ final class MutableChatListView {
actionsSummaryCount = postbox.pendingMessageActionsMetadataTable.getCount(.peerNamespaceAction(key.peerId, key.namespace, key.type))
}
return .MessageEntry(index, renderedMessage, combinedReadState, notificationSettings, embeddedState, RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence, ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount), postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact)
return .MessageEntry(index: index, message: renderedMessage, readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: notificationSettings, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId)?.chatListEmbeddedState, renderedPeer: RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence: presence, tagSummaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount), hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact)
default:
return nil
}
}
}*/
func render(postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) {
for i in 0 ..< self.entries.count {
/*for i in 0 ..< self.entries.count {
if let updatedEntry = self.renderEntry(self.entries[i], postbox: postbox, renderMessage: renderMessage, getPeer: getPeer, getPeerNotificationSettings: getPeerNotificationSettings, getPeerPresence: getPeerPresence) {
self.entries[i] = updatedEntry
}
@ -1089,7 +601,7 @@ final class MutableChatListView {
if let updatedEntry = self.renderEntry(self.additionalMixedPinnedEntries[i], postbox: postbox, renderMessage: renderMessage, getPeer: getPeer, getPeerNotificationSettings: getPeerNotificationSettings, getPeerPresence: getPeerPresence) {
self.additionalMixedPinnedEntries[i] = updatedEntry
}
}
}*/
}
}
@ -1105,7 +617,7 @@ public final class ChatListView {
self.groupId = mutableView.groupId
var entries: [ChatListEntry] = []
for entry in mutableView.entries {
for entry in mutableView.sampledState.entries {
switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
entries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact))
@ -1115,7 +627,7 @@ public final class ChatListView {
assertionFailure()
}
}
if !mutableView.additionalMixedItemEntries.isEmpty || !mutableView.additionalMixedPinnedEntries.isEmpty {
/*if !mutableView.additionalMixedItemEntries.isEmpty || !mutableView.additionalMixedPinnedEntries.isEmpty {
var existingIds = Set<PeerId>()
for entry in entries {
if case let .MessageEntry(messageEntry) = entry {
@ -1161,14 +673,15 @@ public final class ChatListView {
}
}
entries.sort()
}
self.groupEntries = mutableView.groupEntries
}*/
self.entries = entries
self.earlierIndex = mutableView.earlier?.index
self.laterIndex = mutableView.later?.index
self.earlierIndex = mutableView.sampledState.lower?.index
self.laterIndex = mutableView.sampledState.upper?.index
self.groupEntries = mutableView.groupEntries
var additionalItemEntries: [ChatListEntry] = []
for entry in mutableView.additionalItemEntries {
/*for entry in mutableView.additionalItemEntries {
switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact))
@ -1177,7 +690,7 @@ public final class ChatListView {
case .IntermediateMessageEntry:
assertionFailure()
}
}
}*/
self.additionalItemEntries = additionalItemEntries
}

View File

@ -0,0 +1,994 @@
enum ChatListViewSpacePinned {
case notPinned
case includePinned
case includePinnedAsUnpinned
var include: Bool {
switch self {
case .notPinned:
return false
case .includePinned, .includePinnedAsUnpinned:
return true
}
}
}
enum ChatListViewSpace: Hashable {
case group(groupId: PeerGroupId, pinned: ChatListViewSpacePinned)
}
private func mappedChatListFilterPredicate(postbox: Postbox, groupId: PeerGroupId, predicate: ChatListFilterPredicate) -> (ChatListIntermediateEntry) -> Bool {
return { entry in
switch entry {
case let .message(index, _):
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
let isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId)
if predicate.includes(peer: peer, groupId: groupId, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact) {
return true
} else {
return false
}
} else {
return false
}
case .hole:
return true
}
}
}
private func updateMessagePeers(_ message: Message, updatedPeers: [PeerId: Peer]) -> Message? {
var updated = false
for (peerId, currentPeer) in message.peers {
if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) {
updated = true
break
}
}
if updated {
var peers = SimpleDictionary<PeerId, Peer>()
for (peerId, currentPeer) in message.peers {
if let updatedPeer = updatedPeers[peerId] {
peers[peerId] = updatedPeer
} else {
peers[peerId] = currentPeer
}
}
return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds)
}
return nil
}
private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [PeerId: Peer]) -> RenderedPeer? {
var updated = false
for (peerId, currentPeer) in renderedPeer.peers {
if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) {
updated = true
break
}
}
if updated {
var peers = SimpleDictionary<PeerId, Peer>()
for (peerId, currentPeer) in renderedPeer.peers {
if let updatedPeer = updatedPeers[peerId] {
peers[peerId] = updatedPeer
} else {
peers[peerId] = currentPeer
}
}
return RenderedPeer(peerId: renderedPeer.peerId, peers: peers)
}
return nil
}
private final class ChatListViewSpaceState {
private let space: ChatListViewSpace
private let anchorIndex: MutableChatListEntryIndex
private let filterPredicate: ChatListFilterPredicate?
private let summaryComponents: ChatListEntrySummaryComponents
private let halfLimit: Int
var orderedEntries: OrderedChatListViewEntries
init(postbox: Postbox, space: ChatListViewSpace, anchorIndex: MutableChatListEntryIndex, filterPredicate: ChatListFilterPredicate?, summaryComponents: ChatListEntrySummaryComponents, halfLimit: Int) {
self.space = space
self.anchorIndex = anchorIndex
self.filterPredicate = filterPredicate
self.summaryComponents = summaryComponents
self.halfLimit = halfLimit
self.orderedEntries = OrderedChatListViewEntries(lowerOrAtAnchor: [], higherThanAnchor: [])
self.fillSpace(postbox: postbox)
}
private func fillSpace(postbox: Postbox) {
switch self.space {
case let .group(groupId, pinned):
let lowerBound: MutableChatListEntryIndex
let upperBound: MutableChatListEntryIndex
if pinned.include {
upperBound = .absoluteUpperBound
lowerBound = MutableChatListEntryIndex(index: ChatListIndex.pinnedLowerBound, isMessage: true)
} else {
upperBound = MutableChatListEntryIndex(index: ChatListIndex.pinnedLowerBound.predecessor, isMessage: true)
lowerBound = .absoluteLowerBound
}
let resolvedAnchorIndex = min(upperBound, max(self.anchorIndex, lowerBound))
var lowerOrAtAnchorMessages: [MutableChatListEntry] = self.orderedEntries.lowerOrAtAnchor.reversed()
var higherThanAnchorMessages: [MutableChatListEntry] = self.orderedEntries.higherThanAnchor
func mapEntry(_ entry: ChatListIntermediateEntry) -> MutableChatListEntry {
switch entry {
case let .message(index, messageIndex):
var updatedIndex = index
if case .includePinnedAsUnpinned = pinned {
updatedIndex = ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex)
}
return .IntermediateMessageEntry(index: updatedIndex, messageIndex: messageIndex)
case let .hole(hole):
return .HoleEntry(hole)
}
}
if case .includePinnedAsUnpinned = pinned {
if lowerOrAtAnchorMessages.count < self.halfLimit || higherThanAnchorMessages.count < self.halfLimit {
let loadedMessages = postbox.chatListTable.entries(groupId: groupId, from: (ChatListIndex.pinnedLowerBound, true), to: (ChatListIndex.absoluteUpperBound, true), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: self.halfLimit * 2, predicate: self.filterPredicate.flatMap { mappedChatListFilterPredicate(postbox: postbox, groupId: groupId, predicate: $0) }).map(mapEntry).sorted(by: { $0.entryIndex < $1.entryIndex })
if lowerOrAtAnchorMessages.count < self.halfLimit {
var nextLowerIndex: MutableChatListEntryIndex
if let lastMessage = lowerOrAtAnchorMessages.min(by: { $0.entryIndex < $1.entryIndex }) {
nextLowerIndex = lastMessage.entryIndex.predecessor
} else {
nextLowerIndex = resolvedAnchorIndex
}
var loadedLowerMessages = Array(loadedMessages.filter({ $0.entryIndex <= nextLowerIndex }).reversed())
let lowerLimit = self.halfLimit - lowerOrAtAnchorMessages.count
if loadedLowerMessages.count > lowerLimit {
loadedLowerMessages.removeLast(loadedLowerMessages.count - lowerLimit)
}
lowerOrAtAnchorMessages.append(contentsOf: loadedLowerMessages)
}
if higherThanAnchorMessages.count < self.halfLimit {
var nextHigherIndex: MutableChatListEntryIndex
if let lastMessage = higherThanAnchorMessages.max(by: { $0.entryIndex < $1.entryIndex }) {
nextHigherIndex = lastMessage.entryIndex.successor
} else {
nextHigherIndex = resolvedAnchorIndex
}
var loadedHigherMessages = loadedMessages.filter({ $0.entryIndex >= nextHigherIndex })
let higherLimit = self.halfLimit - higherThanAnchorMessages.count
if loadedHigherMessages.count > higherLimit {
loadedHigherMessages.removeLast(loadedHigherMessages.count - higherLimit)
}
higherThanAnchorMessages.append(contentsOf: loadedHigherMessages)
}
}
} else {
if lowerOrAtAnchorMessages.count < self.halfLimit {
var nextLowerIndex: MutableChatListEntryIndex
if let lastMessage = lowerOrAtAnchorMessages.min(by: { $0.entryIndex < $1.entryIndex }) {
nextLowerIndex = lastMessage.entryIndex.predecessor
} else {
nextLowerIndex = resolvedAnchorIndex
}
let loadedLowerMessages = postbox.chatListTable.entries(groupId: groupId, from: (nextLowerIndex.index, nextLowerIndex.isMessage), to: (lowerBound.index, lowerBound.isMessage), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: self.halfLimit - lowerOrAtAnchorMessages.count, predicate: self.filterPredicate.flatMap { mappedChatListFilterPredicate(postbox: postbox, groupId: groupId, predicate: $0) }).map(mapEntry)
lowerOrAtAnchorMessages.append(contentsOf: loadedLowerMessages)
}
if higherThanAnchorMessages.count < self.halfLimit {
var nextHigherIndex: MutableChatListEntryIndex
if let lastMessage = higherThanAnchorMessages.max(by: { $0.entryIndex < $1.entryIndex }) {
nextHigherIndex = lastMessage.entryIndex.successor
} else {
nextHigherIndex = resolvedAnchorIndex
}
let loadedHigherMessages = postbox.chatListTable.entries(groupId: groupId, from: (nextHigherIndex.index, nextHigherIndex.isMessage), to: (upperBound.index, upperBound.isMessage), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: self.halfLimit - higherThanAnchorMessages.count, predicate: self.filterPredicate.flatMap { mappedChatListFilterPredicate(postbox: postbox, groupId: groupId, predicate: $0) }).map(mapEntry)
higherThanAnchorMessages.append(contentsOf: loadedHigherMessages)
}
}
lowerOrAtAnchorMessages.reverse()
assert(lowerOrAtAnchorMessages.count <= self.halfLimit)
assert(higherThanAnchorMessages.count <= self.halfLimit)
let allIndices = (lowerOrAtAnchorMessages + higherThanAnchorMessages).map { $0.entryIndex }
assert(Set(allIndices).count == allIndices.count)
assert(allIndices.sorted() == allIndices)
let entries = OrderedChatListViewEntries(lowerOrAtAnchor: lowerOrAtAnchorMessages, higherThanAnchor: higherThanAnchorMessages)
self.orderedEntries = entries
}
}
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
var hasUpdates = false
var hadRemovals = false
for (groupId, operations) in transaction.chatListOperations {
let matchesSpace: Bool
switch self.space {
case .group(groupId, _):
matchesSpace = true
default:
matchesSpace = false
}
if !matchesSpace {
continue
}
inner: for operation in operations {
switch operation {
case let .InsertEntry(index, messageIndex):
switch self.space {
case let .group(_, pinned) where (index.pinningIndex != nil) == pinned.include:
var updatedIndex = index
if case .includePinnedAsUnpinned = pinned {
updatedIndex = ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex)
}
if let filterPredicate = self.filterPredicate {
if let peer = postbox.peerTable.get(updatedIndex.messageIndex.id.peerId) {
let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
if !filterPredicate.includes(peer: peer, groupId: groupId, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: postbox.readStateTable.getCombinedState(peer.id)?.isUnread ?? false, isContact: postbox.contactsTable.isContact(peerId: notificationsPeerId)) {
continue inner
}
} else {
continue inner
}
}
if self.add(entry: .IntermediateMessageEntry(index: updatedIndex, messageIndex: messageIndex)) {
hasUpdates = true
}
default:
break
}
case let .InsertHole(hole):
switch self.space {
case let .group(_, pinned) where !pinned.include:
if self.add(entry: .HoleEntry(hole)) {
hasUpdates = true
}
default:
break
}
case let .RemoveEntry(indices):
for index in indices {
var updatedIndex = index
if case .group(_, .includePinnedAsUnpinned) = self.space {
updatedIndex = ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex)
}
if self.orderedEntries.remove(index: MutableChatListEntryIndex(index: updatedIndex, isMessage: true)) {
hasUpdates = true
hadRemovals = true
}
}
case let .RemoveHoles(indices):
for index in indices {
if self.orderedEntries.remove(index: MutableChatListEntryIndex(index: index, isMessage: false)) {
hasUpdates = true
hadRemovals = true
}
}
}
}
}
if !transaction.currentUpdatedPeerNotificationSettings.isEmpty, let filterPredicate = self.filterPredicate, case let .group(groupId, _) = self.space {
var removeEntryIndices: [MutableChatListEntryIndex] = []
let _ = self.orderedEntries.mutableScan { entry in
let entryPeer: Peer
let entryNotificationsPeerId: PeerId
switch entry {
case let .MessageEntry(messageEntry):
if let peer = messageEntry.renderedPeer.peer {
entryPeer = peer
entryNotificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
} else {
return nil
}
case let .IntermediateMessageEntry(intermediateMessageEntry):
if let peer = postbox.peerTable.get(intermediateMessageEntry.index.messageIndex.id.peerId) {
entryPeer = peer
entryNotificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
} else {
return nil
}
case .HoleEntry:
return nil
}
if let settingsChange = transaction.currentUpdatedPeerNotificationSettings[entryNotificationsPeerId] {
let isUnread = postbox.readStateTable.getCombinedState(entryPeer.id)?.isUnread ?? false
let wasIncluded = filterPredicate.includes(peer: entryPeer, groupId: groupId, notificationSettings: settingsChange.0, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: entryNotificationsPeerId))
let isIncluded = filterPredicate.includes(peer: entryPeer, groupId: groupId, notificationSettings: settingsChange.1, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: entryNotificationsPeerId))
if wasIncluded != isIncluded {
if !isIncluded {
removeEntryIndices.append(entry.entryIndex)
}
}
}
return nil
}
if !removeEntryIndices.isEmpty {
hasUpdates = true
hadRemovals = true
for index in removeEntryIndices {
let _ = self.orderedEntries.remove(index: index)
}
}
for (peerId, settingsChange) in transaction.currentUpdatedPeerNotificationSettings {
if let mainPeer = postbox.peerTable.get(peerId) {
var peers: [Peer] = [mainPeer]
for associatedId in postbox.reverseAssociatedPeerTable.get(peerId: mainPeer.id) {
if let associatedPeer = postbox.peerTable.get(associatedId) {
peers.append(associatedPeer)
}
}
assert(Set(peers.map { $0.id }).count == peers.count)
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let wasIncluded = filterPredicate.includes(peer: mainPeer, groupId: groupId, notificationSettings: settingsChange.0, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId))
let isIncluded = filterPredicate.includes(peer: mainPeer, groupId: groupId, notificationSettings: settingsChange.1, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId))
if wasIncluded != isIncluded {
if isIncluded {
for peer in peers {
let tableEntry: ChatListIntermediateEntry?
tableEntry = postbox.chatListTable.getEntry(peerId: peer.id, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable)
if let entry = tableEntry {
switch entry {
case let .message(index, messageIndex):
if self.add(entry: .IntermediateMessageEntry(index: index, messageIndex: messageIndex)) {
hasUpdates = true
}
default:
break
}
}
}
}
}
}
}
}
if !transaction.currentUpdatedPeerNotificationSettings.isEmpty {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(messageEntry):
if let peer = messageEntry.renderedPeer.peer {
let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
if let (_, updated) = transaction.currentUpdatedPeerNotificationSettings[notificationsPeerId] {
return .MessageEntry(index: messageEntry.index, message: messageEntry.message, readState: messageEntry.readState, notificationSettings: updated, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: messageEntry.renderedPeer, presence: messageEntry.presence, tagSummaryInfo: messageEntry.tagSummaryInfo, hasFailedMessages: messageEntry.hasFailedMessages, isContact: messageEntry.isContact)
} else {
return nil
}
} else {
return nil
}
default:
return nil
}
}) {
hasUpdates = true
}
}
if !transaction.updatedFailedMessagePeerIds.isEmpty {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(messageEntry):
if transaction.updatedFailedMessagePeerIds.contains(messageEntry.index.messageIndex.id.peerId) {
return .MessageEntry(index: messageEntry.index, message: messageEntry.message, readState: messageEntry.readState, notificationSettings: messageEntry.notificationSettings, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: messageEntry.renderedPeer, presence: messageEntry.presence, tagSummaryInfo: messageEntry.tagSummaryInfo, hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: messageEntry.index.messageIndex.id.peerId), isContact: messageEntry.isContact)
} else {
return nil
}
default:
return nil
}
}) {
hasUpdates = true
}
}
if !transaction.currentUpdatedPeers.isEmpty {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(messageEntry):
var updatedMessage: Message?
if let message = messageEntry.message {
updatedMessage = updateMessagePeers(message, updatedPeers: transaction.currentUpdatedPeers)
}
let renderedPeer = updatedRenderedPeer(messageEntry.renderedPeer, updatedPeers: transaction.currentUpdatedPeers)
if updatedMessage != nil || renderedPeer != nil {
return .MessageEntry(index: messageEntry.index, message: updatedMessage ?? messageEntry.message, readState: messageEntry.readState, notificationSettings: messageEntry.notificationSettings, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: renderedPeer ?? messageEntry.renderedPeer, presence: messageEntry.presence, tagSummaryInfo: messageEntry.tagSummaryInfo, hasFailedMessages: messageEntry.hasFailedMessages, isContact: messageEntry.isContact)
} else {
return nil
}
default:
return nil
}
}) {
hasUpdates = true
}
}
if !transaction.currentUpdatedPeerPresences.isEmpty {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(messageEntry):
var presencePeerId = messageEntry.renderedPeer.peerId
if let peer = messageEntry.renderedPeer.peers[messageEntry.renderedPeer.peerId], let associatedPeerId = peer.associatedPeerId {
presencePeerId = associatedPeerId
}
if let presence = transaction.currentUpdatedPeerPresences[presencePeerId] {
return .MessageEntry(index: messageEntry.index, message: messageEntry.message, readState: messageEntry.readState, notificationSettings: messageEntry.notificationSettings, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: messageEntry.renderedPeer, presence: presence, tagSummaryInfo: messageEntry.tagSummaryInfo, hasFailedMessages: messageEntry.hasFailedMessages, isContact: messageEntry.isContact)
} else {
return nil
}
default:
return nil
}
}) {
hasUpdates = true
}
}
if !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageActionsSummaries.isEmpty {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(messageEntry):
var updatedTagSummaryCount: Int32?
var updatedActionsSummaryCount: Int32?
if let tagSummary = self.summaryComponents.tagSummary {
let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: messageEntry.index.messageIndex.id.peerId, namespace: tagSummary.namespace)
if let summary = transaction.currentUpdatedMessageTagSummaries[key] {
updatedTagSummaryCount = summary.count
}
}
if let actionsSummary = self.summaryComponents.actionsSummary {
let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: messageEntry.index.messageIndex.id.peerId, namespace: actionsSummary.namespace)
if let count = transaction.currentUpdatedMessageActionsSummaries[key] {
updatedActionsSummaryCount = count
}
}
if updatedTagSummaryCount != nil || updatedActionsSummaryCount != nil {
let summaryInfo = ChatListMessageTagSummaryInfo(tagSummaryCount: updatedTagSummaryCount ?? messageEntry.tagSummaryInfo.tagSummaryCount, actionsSummaryCount: updatedActionsSummaryCount ?? messageEntry.tagSummaryInfo.actionsSummaryCount)
return .MessageEntry(index: messageEntry.index, message: messageEntry.message, readState: messageEntry.readState, notificationSettings: messageEntry.notificationSettings, embeddedInterfaceState: messageEntry.embeddedInterfaceState, renderedPeer: messageEntry.renderedPeer, presence: messageEntry.presence, tagSummaryInfo: summaryInfo, hasFailedMessages: messageEntry.hasFailedMessages, isContact: messageEntry.isContact)
} else {
return nil
}
default:
return nil
}
}) {
hasUpdates = true
}
}
if hadRemovals {
self.fillSpace(postbox: postbox)
}
return hasUpdates
}
private func add(entry: MutableChatListEntry) -> Bool {
if self.anchorIndex >= entry.entryIndex {
let insertionIndex = binaryInsertionIndex(self.orderedEntries.lowerOrAtAnchor, extract: { $0.entryIndex }, searchItem: entry.entryIndex)
if insertionIndex < self.orderedEntries.lowerOrAtAnchor.count {
if self.orderedEntries.lowerOrAtAnchor[insertionIndex].entryIndex == entry.entryIndex {
assertionFailure("Inserting an existing index is not allowed")
self.orderedEntries.setLowerOrAtAnchorAtArrayIndex(insertionIndex, to: entry)
return true
}
}
if insertionIndex == 0 && self.orderedEntries.lowerOrAtAnchor.count >= self.halfLimit {
return false
}
self.orderedEntries.insertLowerOrAtAnchorAtArrayIndex(insertionIndex, value: entry)
if self.orderedEntries.lowerOrAtAnchor.count > self.halfLimit {
self.orderedEntries.removeLowerOrAtAnchorAtArrayIndex(0)
}
return true
} else {
let insertionIndex = binaryInsertionIndex(orderedEntries.higherThanAnchor, extract: { $0.entryIndex }, searchItem: entry.entryIndex)
if insertionIndex < self.orderedEntries.higherThanAnchor.count {
if self.orderedEntries.higherThanAnchor[insertionIndex].entryIndex == entry.entryIndex {
assertionFailure("Inserting an existing index is not allowed")
self.orderedEntries.setHigherThanAnchorAtArrayIndex(insertionIndex, to: entry)
return true
}
}
if insertionIndex == self.orderedEntries.higherThanAnchor.count && self.orderedEntries.higherThanAnchor.count >= self.halfLimit {
return false
}
self.orderedEntries.insertHigherThanAnchorAtArrayIndex(insertionIndex, value: entry)
if self.orderedEntries.higherThanAnchor.count > self.halfLimit {
self.orderedEntries.removeHigherThanAnchorAtArrayIndex(self.orderedEntries.higherThanAnchor.count - 1)
}
return true
}
}
}
private struct MutableChatListEntryIndex: Hashable, Comparable {
var index: ChatListIndex
var isMessage: Bool
var predecessor: MutableChatListEntryIndex {
return MutableChatListEntryIndex(index: self.index.predecessor, isMessage: true)
}
var successor: MutableChatListEntryIndex {
return MutableChatListEntryIndex(index: self.index.successor, isMessage: true)
}
static let absoluteLowerBound = MutableChatListEntryIndex(index: .absoluteLowerBound, isMessage: true)
static let absoluteUpperBound = MutableChatListEntryIndex(index: .absoluteUpperBound, isMessage: true)
static func <(lhs: MutableChatListEntryIndex, rhs: MutableChatListEntryIndex) -> Bool {
if lhs.index != rhs.index {
return lhs.index < rhs.index
} else if lhs.isMessage != rhs.isMessage {
return lhs.isMessage
} else {
return false
}
}
}
private extension MutableChatListEntry {
var messagePeerId: PeerId? {
switch self {
case let .IntermediateMessageEntry(intermediateMessageEntry):
return intermediateMessageEntry.0.messageIndex.id.peerId
case let .MessageEntry(messageEntry):
return messageEntry.0.messageIndex.id.peerId
case .HoleEntry:
return nil
}
}
var entryIndex: MutableChatListEntryIndex {
switch self {
case let .IntermediateMessageEntry(intermediateMessageEntry):
return MutableChatListEntryIndex(index: intermediateMessageEntry.index, isMessage: true)
case let .MessageEntry(messageEntry):
return MutableChatListEntryIndex(index: messageEntry.index, isMessage: true)
case let .HoleEntry(hole):
return MutableChatListEntryIndex(index: ChatListIndex(pinningIndex: nil, messageIndex: hole.index), isMessage: false)
}
}
}
private struct OrderedChatListViewEntries {
private(set) var lowerOrAtAnchor: [MutableChatListEntry]
private(set) var higherThanAnchor: [MutableChatListEntry]
private(set) var reverseIndices: [PeerId: [MutableChatListEntryIndex]] = [:]
fileprivate init(lowerOrAtAnchor: [MutableChatListEntry], higherThanAnchor: [MutableChatListEntry]) {
self.lowerOrAtAnchor = lowerOrAtAnchor
self.higherThanAnchor = higherThanAnchor
for entry in lowerOrAtAnchor {
if let peerId = entry.messagePeerId {
if self.reverseIndices[peerId] == nil {
self.reverseIndices[peerId] = [entry.entryIndex]
} else {
self.reverseIndices[peerId]!.append(entry.entryIndex)
}
}
}
for entry in higherThanAnchor {
if let peerId = entry.messagePeerId {
if self.reverseIndices[peerId] == nil {
self.reverseIndices[peerId] = [entry.entryIndex]
} else {
self.reverseIndices[peerId]!.append(entry.entryIndex)
}
}
}
}
mutating func setLowerOrAtAnchorAtArrayIndex(_ index: Int, to value: MutableChatListEntry) {
let previousIndex = self.lowerOrAtAnchor[index].entryIndex
let updatedIndex = value.entryIndex
let previousPeerId = self.lowerOrAtAnchor[index].messagePeerId
let updatedPeerId = value.messagePeerId
self.lowerOrAtAnchor[index] = value
if previousPeerId != updatedPeerId {
if let previousPeerId = previousPeerId {
self.reverseIndices[previousPeerId]?.removeAll(where: { $0 == previousIndex })
if let isEmpty = self.reverseIndices[previousPeerId]?.isEmpty, isEmpty {
self.reverseIndices.removeValue(forKey: previousPeerId)
}
}
if let updatedPeerId = updatedPeerId {
if self.reverseIndices[updatedPeerId] == nil {
self.reverseIndices[updatedPeerId] = [updatedIndex]
} else {
self.reverseIndices[updatedPeerId]!.append(updatedIndex)
}
}
}
}
mutating func setHigherThanAnchorAtArrayIndex(_ index: Int, to value: MutableChatListEntry) {
let previousIndex = self.higherThanAnchor[index].entryIndex
let updatedIndex = value.entryIndex
let previousPeerId = self.higherThanAnchor[index].messagePeerId
let updatedPeerId = value.messagePeerId
self.higherThanAnchor[index] = value
if previousPeerId != updatedPeerId {
if let previousPeerId = previousPeerId {
self.reverseIndices[previousPeerId]?.removeAll(where: { $0 == previousIndex })
if let isEmpty = self.reverseIndices[previousPeerId]?.isEmpty, isEmpty {
self.reverseIndices.removeValue(forKey: previousPeerId)
}
}
if let updatedPeerId = updatedPeerId {
if self.reverseIndices[updatedPeerId] == nil {
self.reverseIndices[updatedPeerId] = [updatedIndex]
} else {
self.reverseIndices[updatedPeerId]!.append(updatedIndex)
}
}
}
}
mutating func insertLowerOrAtAnchorAtArrayIndex(_ index: Int, value: MutableChatListEntry) {
self.lowerOrAtAnchor.insert(value, at: index)
if let peerId = value.messagePeerId {
if self.reverseIndices[peerId] == nil {
self.reverseIndices[peerId] = [value.entryIndex]
} else {
self.reverseIndices[peerId]!.append(value.entryIndex)
}
}
}
mutating func insertHigherThanAnchorAtArrayIndex(_ index: Int, value: MutableChatListEntry) {
self.higherThanAnchor.insert(value, at: index)
if let peerId = value.messagePeerId {
if self.reverseIndices[peerId] == nil {
self.reverseIndices[peerId] = [value.entryIndex]
} else {
self.reverseIndices[peerId]!.append(value.entryIndex)
}
}
}
mutating func removeLowerOrAtAnchorAtArrayIndex(_ index: Int) {
let previousIndex = self.lowerOrAtAnchor[index].entryIndex
if let peerId = self.lowerOrAtAnchor[index].messagePeerId {
self.reverseIndices[peerId]?.removeAll(where: { $0 == previousIndex })
if let isEmpty = self.reverseIndices[peerId]?.isEmpty, isEmpty {
self.reverseIndices.removeValue(forKey: peerId)
}
}
self.lowerOrAtAnchor.remove(at: index)
}
mutating func removeHigherThanAnchorAtArrayIndex(_ index: Int) {
let previousIndex = self.higherThanAnchor[index].entryIndex
if let peerId = self.higherThanAnchor[index].messagePeerId {
self.reverseIndices[peerId]?.removeAll(where: { $0 == previousIndex })
if let isEmpty = self.reverseIndices[peerId]?.isEmpty, isEmpty {
self.reverseIndices.removeValue(forKey: peerId)
}
}
self.higherThanAnchor.remove(at: index)
}
func find(index: MutableChatListEntryIndex) -> MutableChatListEntry? {
if let entryIndex = binarySearch(self.lowerOrAtAnchor, extract: { $0.entryIndex }, searchItem: index) {
return self.lowerOrAtAnchor[entryIndex]
} else if let entryIndex = binarySearch(self.higherThanAnchor, extract: { $0.entryIndex }, searchItem: index) {
return self.higherThanAnchor[entryIndex]
} else {
return nil
}
}
func indicesForPeerId(_ peerId: PeerId) -> [MutableChatListEntryIndex]? {
return self.reverseIndices[peerId]
}
var first: MutableChatListEntry? {
return self.lowerOrAtAnchor.first ?? self.higherThanAnchor.first
}
mutating func mutableScan(_ f: (MutableChatListEntry) -> MutableChatListEntry?) -> Bool {
var anyUpdated = false
for i in 0 ..< self.lowerOrAtAnchor.count {
if let updated = f(self.lowerOrAtAnchor[i]) {
self.setLowerOrAtAnchorAtArrayIndex(i, to: updated)
anyUpdated = true
}
}
for i in 0 ..< self.higherThanAnchor.count {
if let updated = f(self.higherThanAnchor[i]) {
self.setHigherThanAnchorAtArrayIndex(i, to: updated)
anyUpdated = true
}
}
return anyUpdated
}
mutating func update(index: MutableChatListEntryIndex, _ f: (MutableChatListEntry) -> MutableChatListEntry?) -> Bool {
if let entryIndex = binarySearch(self.lowerOrAtAnchor, extract: { $0.entryIndex }, searchItem: index) {
if let updated = f(self.lowerOrAtAnchor[entryIndex]) {
self.setLowerOrAtAnchorAtArrayIndex(entryIndex, to: updated)
return true
}
} else if let entryIndex = binarySearch(self.higherThanAnchor, extract: { $0.entryIndex }, searchItem: index) {
if let updated = f(self.higherThanAnchor[entryIndex]) {
self.setHigherThanAnchorAtArrayIndex(entryIndex, to: updated)
return true
}
}
return false
}
mutating func remove(index: MutableChatListEntryIndex) -> Bool {
if let entryIndex = binarySearch(self.lowerOrAtAnchor, extract: { $0.entryIndex }, searchItem: index) {
self.removeLowerOrAtAnchorAtArrayIndex(entryIndex)
return true
} else if let entryIndex = binarySearch(self.higherThanAnchor, extract: { $0.entryIndex }, searchItem: index) {
self.removeHigherThanAnchorAtArrayIndex(entryIndex)
return true
} else {
return false
}
}
}
final class ChatListViewSample {
let entries: [MutableChatListEntry]
let lower: MutableChatListEntry?
let upper: MutableChatListEntry?
let anchorIndex: ChatListIndex
let hole: ChatListHole?
fileprivate init(entries: [MutableChatListEntry], lower: MutableChatListEntry?, upper: MutableChatListEntry?, anchorIndex: ChatListIndex, hole: ChatListHole?) {
self.entries = entries
self.lower = lower
self.upper = upper
self.anchorIndex = anchorIndex
self.hole = hole
}
}
struct ChatListViewState {
private let anchorIndex: MutableChatListEntryIndex
private let filterPredicate: ChatListFilterPredicate?
private let summaryComponents: ChatListEntrySummaryComponents
private let halfLimit: Int
private var stateBySpace: [ChatListViewSpace: ChatListViewSpaceState] = [:]
init(postbox: Postbox, spaces: [ChatListViewSpace], anchorIndex: ChatListIndex, filterPredicate: ChatListFilterPredicate?, summaryComponents: ChatListEntrySummaryComponents, halfLimit: Int) {
self.anchorIndex = MutableChatListEntryIndex(index: anchorIndex, isMessage: true)
self.filterPredicate = filterPredicate
self.summaryComponents = summaryComponents
self.halfLimit = halfLimit
for space in spaces {
self.stateBySpace[space] = ChatListViewSpaceState(postbox: postbox, space: space, anchorIndex: self.anchorIndex, filterPredicate: self.filterPredicate, summaryComponents: summaryComponents, halfLimit: halfLimit)
}
}
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
var updated = false
for (_, state) in self.stateBySpace {
if state.replay(postbox: postbox, transaction: transaction) {
updated = true
}
}
return updated
}
private func sampleIndices() -> (lowerOrAtAnchor: [(ChatListViewSpace, Int)], higherThanAnchor: [(ChatListViewSpace, Int)]) {
var previousAnchorIndices: [ChatListViewSpace: Int] = [:]
var nextAnchorIndices: [ChatListViewSpace: Int] = [:]
for (space, state) in self.stateBySpace {
previousAnchorIndices[space] = state.orderedEntries.lowerOrAtAnchor.count - 1
nextAnchorIndices[space] = 0
}
var backwardsResult: [(ChatListViewSpace, Int)] = []
var result: [(ChatListViewSpace, Int)] = []
while true {
var minSpace: ChatListViewSpace?
for (space, value) in previousAnchorIndices {
if value != -1 {
if let minSpaceValue = minSpace {
if self.stateBySpace[space]!.orderedEntries.lowerOrAtAnchor[value].entryIndex > self.stateBySpace[minSpaceValue]!.orderedEntries.lowerOrAtAnchor[previousAnchorIndices[minSpaceValue]!].entryIndex {
minSpace = space
}
} else {
minSpace = space
}
}
}
if let minSpace = minSpace {
backwardsResult.append((minSpace, previousAnchorIndices[minSpace]!))
previousAnchorIndices[minSpace]! -= 1
if backwardsResult.count == self.halfLimit {
break
}
}
if minSpace == nil {
break
}
}
while true {
var maxSpace: ChatListViewSpace?
for (space, value) in nextAnchorIndices {
if value != self.stateBySpace[space]!.orderedEntries.higherThanAnchor.count {
if let maxSpaceValue = maxSpace {
if self.stateBySpace[space]!.orderedEntries.higherThanAnchor[value].entryIndex < self.stateBySpace[maxSpaceValue]!.orderedEntries.higherThanAnchor[nextAnchorIndices[maxSpaceValue]!].entryIndex {
maxSpace = space
}
} else {
maxSpace = space
}
}
}
if let maxSpace = maxSpace {
result.append((maxSpace, nextAnchorIndices[maxSpace]!))
nextAnchorIndices[maxSpace]! += 1
if result.count == self.halfLimit {
break
}
}
if maxSpace == nil {
break
}
}
return (backwardsResult.reversed(), result)
}
func sample(postbox: Postbox) -> ChatListViewSample {
let combinedSpacesAndIndicesByDirection = self.sampleIndices()
var result: [MutableChatListEntry] = []
var sampledHoleIndices: [Int] = []
var sampledAnchorBoundaryIndex: Int?
let directions = [combinedSpacesAndIndicesByDirection.lowerOrAtAnchor, combinedSpacesAndIndicesByDirection.higherThanAnchor]
for directionIndex in 0 ..< directions.count {
outer: for i in 0 ..< directions[directionIndex].count {
let (space, listIndex) = directions[directionIndex][i]
let entry: MutableChatListEntry
if directionIndex == 0 {
entry = self.stateBySpace[space]!.orderedEntries.lowerOrAtAnchor[listIndex]
} else {
entry = self.stateBySpace[space]!.orderedEntries.higherThanAnchor[listIndex]
}
if entry.entryIndex >= self.anchorIndex {
sampledAnchorBoundaryIndex = result.count
}
switch entry {
case let .IntermediateMessageEntry(index, messageIndex):
var peers = SimpleDictionary<PeerId, Peer>()
var notificationsPeerId = index.messageIndex.id.peerId
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
peers[peer.id] = peer
if let notificationSettingsPeerId = peer.notificationSettingsPeerId {
notificationsPeerId = notificationSettingsPeerId
}
if let associatedPeerId = peer.associatedPeerId {
if let associatedPeer = postbox.peerTable.get(associatedPeerId) {
peers[associatedPeer.id] = associatedPeer
}
}
}
let renderedPeer = RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers)
var tagSummaryCount: Int32?
var actionsSummaryCount: Int32?
if let tagSummary = self.summaryComponents.tagSummary {
let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: index.messageIndex.id.peerId, namespace: tagSummary.namespace)
if let summary = postbox.messageHistoryTagsSummaryTable.get(key) {
tagSummaryCount = summary.count
}
}
if let actionsSummary = self.summaryComponents.actionsSummary {
let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: index.messageIndex.id.peerId, namespace: actionsSummary.namespace)
actionsSummaryCount = postbox.pendingMessageActionsMetadataTable.getCount(.peerNamespaceAction(key.peerId, key.namespace, key.type))
}
let tagSummaryInfo = ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount)
let updatedEntry: MutableChatListEntry = .MessageEntry(index: index, message: messageIndex.flatMap(postbox.messageHistoryTable.getMessage).flatMap(postbox.renderIntermediateMessage), readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId)?.chatListEmbeddedState, renderedPeer: renderedPeer, presence: postbox.peerPresenceTable.get(index.messageIndex.id.peerId), tagSummaryInfo: tagSummaryInfo, hasFailedMessages: false, isContact: postbox.contactsTable.isContact(peerId: index.messageIndex.id.peerId))
if directionIndex == 0 {
self.stateBySpace[space]!.orderedEntries.setLowerOrAtAnchorAtArrayIndex(listIndex, to: updatedEntry)
} else {
self.stateBySpace[space]!.orderedEntries.setHigherThanAnchorAtArrayIndex(listIndex, to: updatedEntry)
}
result.append(updatedEntry)
case .MessageEntry:
result.append(entry)
case .HoleEntry:
sampledHoleIndices.append(result.count)
result.append(entry)
}
}
}
let allIndices = result.map { $0.entryIndex }
assert(Set(allIndices).count == allIndices.count)
assert(allIndices.sorted() == allIndices)
var sampledHoleIndex: Int?
if !sampledHoleIndices.isEmpty {
if let sampledAnchorBoundaryIndex = sampledAnchorBoundaryIndex {
var found = false
for i in 0 ..< sampledHoleIndices.count {
if i >= sampledAnchorBoundaryIndex {
sampledHoleIndex = sampledHoleIndices[i]
found = true
break
}
}
if !found {
sampledHoleIndex = sampledHoleIndices.first
}
} else if let index = sampledHoleIndices.first {
sampledHoleIndex = index
}
}
var sampledHole: ChatListHole?
if let index = sampledHoleIndex {
if case let .HoleEntry(hole) = result[index] {
sampledHole = hole
} else {
assertionFailure()
}
}
var lower: MutableChatListEntry?
if combinedSpacesAndIndicesByDirection.lowerOrAtAnchor.count >= self.halfLimit {
lower = result[0]
result.removeFirst()
}
var upper: MutableChatListEntry?
if combinedSpacesAndIndicesByDirection.higherThanAnchor.count >= self.halfLimit {
upper = result.last
result.removeLast()
}
return ChatListViewSample(entries: result, lower: lower, upper: upper, anchorIndex: self.anchorIndex.index, hole: sampledHole)
}
}

View File

@ -184,10 +184,6 @@ public struct ChatListIndex: Comparable, Hashable {
return lhs.messageIndex < rhs.messageIndex
}
public var hashValue: Int {
return self.messageIndex.hashValue
}
public static var absoluteUpperBound: ChatListIndex {
return ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex.absoluteUpperBound())
}
@ -196,6 +192,10 @@ public struct ChatListIndex: Comparable, Hashable {
return ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex.absoluteLowerBound())
}
public static var pinnedLowerBound: ChatListIndex {
return ChatListIndex(pinningIndex: UInt16(Int8.max - 1), messageIndex: MessageIndex.absoluteLowerBound())
}
public var predecessor: ChatListIndex {
return ChatListIndex(pinningIndex: self.pinningIndex, messageIndex: self.messageIndex.predecessor())
}

View File

@ -625,7 +625,7 @@ final class MessageHistoryTable: Table {
return messageIds
}
func topMessage(_ peerId: PeerId) -> IntermediateMessage? {
func topIndex(peerId: PeerId) -> MessageIndex? {
var topIndex: MessageIndex?
for namespace in self.messageHistoryIndexTable.existingNamespaces(peerId: peerId) where self.seedConfiguration.chatMessagesNamespaces.contains(namespace) {
self.valueBox.range(self.table, start: self.upperBound(peerId: peerId, namespace: namespace), end: self.lowerBound(peerId: peerId, namespace: namespace), keys: { key in
@ -641,7 +641,11 @@ final class MessageHistoryTable: Table {
}, limit: 1)
}
return topIndex.flatMap(self.getMessage)
return topIndex
}
func topMessage(peerId: PeerId) -> IntermediateMessage? {
return self.topIndex(peerId: peerId).flatMap(self.getMessage)
}
func exists(index: MessageIndex) -> Bool {

View File

@ -177,7 +177,7 @@ private func binaryIndexOrLower(_ inputArr: [MutableMessageHistoryEntry], _ sear
return hi
}
private func sampleEntries(orderedEntriesBySpace: [PeerIdAndNamespace: OrderedHistoryViewEntries], anchor: HistoryViewAnchor, halfLimit: Int) -> (lowerOrAtAnchor:[(PeerIdAndNamespace, Int)], higherThanAnchor: [(PeerIdAndNamespace, Int)]) {
private func sampleEntries(orderedEntriesBySpace: [PeerIdAndNamespace: OrderedHistoryViewEntries], anchor: HistoryViewAnchor, halfLimit: Int) -> (lowerOrAtAnchor: [(PeerIdAndNamespace, Int)], higherThanAnchor: [(PeerIdAndNamespace, Int)]) {
var previousAnchorIndices: [PeerIdAndNamespace: Int] = [:]
var nextAnchorIndices: [PeerIdAndNamespace: Int] = [:]
for (space, items) in orderedEntriesBySpace {
@ -1051,7 +1051,7 @@ final class HistoryViewLoadedState {
if let associatedIndices = self.orderedEntriesBySpace[space]!.indicesForAssociatedMessageId(entry.index.id) {
for associatedIndex in associatedIndices {
self.orderedEntriesBySpace[space]!.update(index: associatedIndex, { current in
let _ = self.orderedEntriesBySpace[space]!.update(index: associatedIndex, { current in
switch current {
case .IntermediateMessageEntry:
return current

View File

@ -772,36 +772,28 @@ public final class Transaction {
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
let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId)
if predicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact, isArchived: false) {
includedPeerIds[peer.id] = true
return true
var count = 0
var groupIds: [PeerGroupId] = [.root]
groupIds.append(contentsOf: predicate.includeAdditionalPeerGroupIds)
for groupId in groupIds {
count += postbox.chatListTable.countWithPredicate(groupId: groupId, predicate: { peerId in
if let peer = postbox.peerTable.get(peerId) {
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let notificationsPeerId = peer.notificationSettingsPeerId ?? peerId
let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId)
if predicate.includes(peer: peer, groupId: groupId, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact) {
includedPeerIds[peer.id] = true
return true
} else {
return false
}
} else {
return false
}
} else {
return false
}
})
let archivedCount = postbox.chatListTable.countWithPredicate(groupId: .group(1), predicate: { peerId in
if let peer = postbox.peerTable.get(peerId) {
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let notificationsPeerId = peer.notificationSettingsPeerId ?? peerId
let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId)
if predicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact, isArchived: false) {
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 {
@ -809,7 +801,7 @@ public final class Transaction {
}
}
}
return count + archivedCount
return count
}
public func legacyGetAccessChallengeData() -> PostboxAccessChallengeData {
@ -1647,7 +1639,7 @@ public final class Postbox {
switch peerIds {
case let .associated(_, messageId):
if let messageId = messageId, let readState = self.readStateTable.getCombinedState(messageId.peerId), readState.count != 0 {
if let topMessage = self.messageHistoryTable.topMessage(messageId.peerId) {
if let topMessage = self.messageHistoryTable.topMessage(peerId: messageId.peerId) {
let _ = self.messageHistoryTable.applyInteractiveMaxReadIndex(postbox: self, messageIndex: topMessage.index, operationsByPeerId: &self.currentOperationsByPeerId, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations)
}
}
@ -1702,71 +1694,6 @@ public final class Postbox {
self.synchronizeGroupMessageStatsTable.set(groupId: groupId, namespace: namespace, needsValidation: false, operations: &self.currentUpdatedGroupSummarySynchronizeOperations)
}
private func mappedChatListFilterPredicate(_ predicate: ChatListFilterPredicate, isArchived: Bool) -> (ChatListIntermediateEntry) -> Bool {
return { entry in
switch entry {
case let .message(index, _, _):
if index.pinningIndex != nil {
return false
}
if let peer = self.peerTable.get(index.messageIndex.id.peerId) {
let isUnread = self.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
let isContact = self.contactsTable.isContact(peerId: notificationsPeerId)
if predicate.includes(peer: peer, notificationSettings: self.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact, isArchived: isArchived) {
return true
} else {
return false
}
} else {
return false
}
case .hole:
return true
}
}
}
func fetchAroundChatEntries(groupId: PeerGroupId, index: ChatListIndex, count: Int, filterPredicate: ChatListFilterPredicate?) -> (entries: [MutableChatListEntry], earlier: MutableChatListEntry?, later: MutableChatListEntry?) {
let mappedPredicate = filterPredicate.flatMap { predicate in
self.mappedChatListFilterPredicate(predicate, isArchived: groupId != .root)
}
let (intermediateEntries, intermediateLower, intermediateUpper) = self.chatListTable.entriesAround(groupId: groupId, index: index, messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
}
let lower: MutableChatListEntry? = intermediateLower.flatMap { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
}
let upper: MutableChatListEntry? = intermediateUpper.flatMap { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
}
return (entries, lower, upper)
}
func fetchEarlierChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ChatListFilterPredicate?) -> [MutableChatListEntry] {
let mappedPredicate = filterPredicate.flatMap { predicate in
self.mappedChatListFilterPredicate(predicate, isArchived: groupId != .root)
}
let intermediateEntries = self.chatListTable.earlierEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
}
return entries
}
func fetchLaterChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ChatListFilterPredicate?) -> [MutableChatListEntry] {
let mappedPredicate = filterPredicate.flatMap { predicate in
self.mappedChatListFilterPredicate(predicate, isArchived: groupId != .root)
}
let intermediateEntries = self.chatListTable.laterEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
}
return entries
}
func renderIntermediateMessage(_ message: IntermediateMessage) -> Message {
let renderedMessage = self.messageHistoryTable.renderMessage(message, peerTable: self.peerTable)

View File

@ -1517,14 +1517,18 @@ public final class SqliteValueBox: ValueBox {
switch result {
case .accept:
acceptedCount += 1
return true
if limit > 0 && acceptedCount >= limit {
hadStop = true
return false
} else {
return true
}
case .skip:
return true
case .stop:
hadStop = true
return false
}
return true
}, limit: limit)
if let lastKey = lastKey {
currentStart = lastKey

View File

@ -26,16 +26,14 @@ public struct ChatListFilterPeerCategories: OptionSet, Hashable {
public static let contacts = ChatListFilterPeerCategories(rawValue: 1 << 0)
public static let nonContacts = ChatListFilterPeerCategories(rawValue: 1 << 1)
public static let smallGroups = ChatListFilterPeerCategories(rawValue: 1 << 2)
public static let largeGroups = ChatListFilterPeerCategories(rawValue: 1 << 3)
public static let channels = ChatListFilterPeerCategories(rawValue: 1 << 4)
public static let bots = ChatListFilterPeerCategories(rawValue: 1 << 5)
public static let groups = ChatListFilterPeerCategories(rawValue: 1 << 2)
public static let channels = ChatListFilterPeerCategories(rawValue: 1 << 3)
public static let bots = ChatListFilterPeerCategories(rawValue: 1 << 4)
public static let all: ChatListFilterPeerCategories = [
.contacts,
.nonContacts,
.smallGroups,
.largeGroups,
.groups,
.channels,
.bots
]
@ -50,10 +48,9 @@ private struct ChatListFilterPeerApiCategories: OptionSet {
static let contacts = ChatListFilterPeerApiCategories(rawValue: 1 << 0)
static let nonContacts = ChatListFilterPeerApiCategories(rawValue: 1 << 1)
static let smallGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 2)
static let largeGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 3)
static let channels = ChatListFilterPeerApiCategories(rawValue: 1 << 4)
static let bots = ChatListFilterPeerApiCategories(rawValue: 1 << 5)
static let groups = ChatListFilterPeerApiCategories(rawValue: 1 << 2)
static let channels = ChatListFilterPeerApiCategories(rawValue: 1 << 3)
static let bots = ChatListFilterPeerApiCategories(rawValue: 1 << 4)
}
extension ChatListFilterPeerCategories {
@ -66,11 +63,8 @@ extension ChatListFilterPeerCategories {
if flags.contains(.nonContacts) {
result.insert(.nonContacts)
}
if flags.contains(.smallGroups) {
result.insert(.smallGroups)
}
if flags.contains(.largeGroups) {
result.insert(.largeGroups)
if flags.contains(.groups) {
result.insert(.groups)
}
if flags.contains(.channels) {
result.insert(.channels)
@ -89,11 +83,8 @@ extension ChatListFilterPeerCategories {
if self.contains(.nonContacts) {
result.insert(.nonContacts)
}
if self.contains(.smallGroups) {
result.insert(.smallGroups)
}
if self.contains(.largeGroups) {
result.insert(.largeGroups)
if self.contains(.groups) {
result.insert(.groups)
}
if self.contains(.channels) {
result.insert(.channels)

View File

@ -256,7 +256,9 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
segmentedBackgroundColor: UIColor(rgb: 0x3a3b3d),
segmentedForegroundColor: UIColor(rgb: 0x6f7075),
segmentedTextColor: UIColor(rgb: 0xffffff),
segmentedDividerColor: UIColor(rgb: 0x505155)
segmentedDividerColor: UIColor(rgb: 0x505155),
clearButtonBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1),
clearButtonForegroundColor: UIColor(rgb: 0xffffff)
)
let navigationSearchBar = PresentationThemeNavigationSearchBar(

View File

@ -503,7 +503,9 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres
segmentedBackgroundColor: mainInputColor,
segmentedForegroundColor: mainBackgroundColor,
segmentedTextColor: UIColor(rgb: 0xffffff),
segmentedDividerColor: mainSecondaryTextColor.withAlphaComponent(0.5)
segmentedDividerColor: mainSecondaryTextColor.withAlphaComponent(0.5),
clearButtonBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1),
clearButtonForegroundColor: UIColor(rgb: 0xffffff)
)
let navigationSearchBar = PresentationThemeNavigationSearchBar(

View File

@ -360,7 +360,9 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
segmentedBackgroundColor: UIColor(rgb: 0xe9e9e9),
segmentedForegroundColor: UIColor(rgb: 0xf7f7f7),
segmentedTextColor: UIColor(rgb: 0x000000),
segmentedDividerColor: UIColor(rgb: 0xd6d6dc)
segmentedDividerColor: UIColor(rgb: 0xd6d6dc),
clearButtonBackgroundColor: UIColor(rgb: 0xE3E3E3, alpha: 0.78),
clearButtonForegroundColor: UIColor(rgb: 0x7f7f7f)
)
let navigationSearchBar = PresentationThemeNavigationSearchBar(

View File

@ -126,8 +126,10 @@ public final class PresentationThemeRootNavigationBar {
public let segmentedForegroundColor: UIColor
public let segmentedTextColor: UIColor
public let segmentedDividerColor: UIColor
public let clearButtonBackgroundColor: UIColor
public let clearButtonForegroundColor: UIColor
public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, segmentedBackgroundColor: UIColor, segmentedForegroundColor: UIColor, segmentedTextColor: UIColor, segmentedDividerColor: UIColor) {
public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, segmentedBackgroundColor: UIColor, segmentedForegroundColor: UIColor, segmentedTextColor: UIColor, segmentedDividerColor: UIColor, clearButtonBackgroundColor: UIColor, clearButtonForegroundColor: UIColor) {
self.buttonColor = buttonColor
self.disabledButtonColor = disabledButtonColor
self.primaryTextColor = primaryTextColor
@ -143,10 +145,14 @@ public final class PresentationThemeRootNavigationBar {
self.segmentedForegroundColor = segmentedForegroundColor
self.segmentedTextColor = segmentedTextColor
self.segmentedDividerColor = segmentedDividerColor
self.clearButtonBackgroundColor = clearButtonBackgroundColor
self.clearButtonForegroundColor = clearButtonForegroundColor
}
public func withUpdated(buttonColor: UIColor? = nil, disabledButtonColor: UIColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, controlColor: UIColor? = nil, accentTextColor: UIColor? = nil, backgroundColor: UIColor? = nil, separatorColor: UIColor? = nil, badgeBackgroundColor: UIColor? = nil, badgeStrokeColor: UIColor? = nil, badgeTextColor: UIColor? = nil, segmentedBackgroundColor: UIColor? = nil, segmentedForegroundColor: UIColor? = nil, segmentedTextColor: UIColor? = nil, segmentedDividerColor: UIColor? = nil) -> PresentationThemeRootNavigationBar {
return PresentationThemeRootNavigationBar(buttonColor: buttonColor ?? self.buttonColor, disabledButtonColor: disabledButtonColor ?? self.disabledButtonColor, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, controlColor: controlColor ?? self.controlColor, accentTextColor: accentTextColor ?? self.accentTextColor, backgroundColor: backgroundColor ?? self.backgroundColor, separatorColor: separatorColor ?? self.separatorColor, badgeBackgroundColor: badgeBackgroundColor ?? self.badgeBackgroundColor, badgeStrokeColor: badgeStrokeColor ?? self.badgeStrokeColor, badgeTextColor: badgeTextColor ?? self.badgeTextColor, segmentedBackgroundColor: segmentedBackgroundColor ?? self.segmentedBackgroundColor, segmentedForegroundColor: segmentedForegroundColor ?? self.segmentedForegroundColor, segmentedTextColor: segmentedTextColor ?? self.segmentedTextColor, segmentedDividerColor: segmentedDividerColor ?? self.segmentedDividerColor)
public func withUpdated(buttonColor: UIColor? = nil, disabledButtonColor: UIColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, controlColor: UIColor? = nil, accentTextColor: UIColor? = nil, backgroundColor: UIColor? = nil, separatorColor: UIColor? = nil, badgeBackgroundColor: UIColor? = nil, badgeStrokeColor: UIColor? = nil, badgeTextColor: UIColor? = nil, segmentedBackgroundColor: UIColor? = nil, segmentedForegroundColor: UIColor? = nil, segmentedTextColor: UIColor? = nil, segmentedDividerColor: UIColor? = nil, clearButtonBackgroundColor: UIColor? = nil, clearButtonForegroundColor: UIColor? = nil) -> PresentationThemeRootNavigationBar {
let resolvedClearButtonBackgroundColor = clearButtonBackgroundColor ?? self.clearButtonBackgroundColor
let resolvedClearButtonForegroundColor = clearButtonForegroundColor ?? self.clearButtonForegroundColor
return PresentationThemeRootNavigationBar(buttonColor: buttonColor ?? self.buttonColor, disabledButtonColor: disabledButtonColor ?? self.disabledButtonColor, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, controlColor: controlColor ?? self.controlColor, accentTextColor: accentTextColor ?? self.accentTextColor, backgroundColor: backgroundColor ?? self.backgroundColor, separatorColor: separatorColor ?? self.separatorColor, badgeBackgroundColor: badgeBackgroundColor ?? self.badgeBackgroundColor, badgeStrokeColor: badgeStrokeColor ?? self.badgeStrokeColor, badgeTextColor: badgeTextColor ?? self.badgeTextColor, segmentedBackgroundColor: segmentedBackgroundColor ?? self.segmentedBackgroundColor, segmentedForegroundColor: segmentedForegroundColor ?? self.segmentedForegroundColor, segmentedTextColor: segmentedTextColor ?? self.segmentedTextColor, segmentedDividerColor: segmentedDividerColor ?? self.segmentedDividerColor, clearButtonBackgroundColor: resolvedClearButtonBackgroundColor, clearButtonForegroundColor: resolvedClearButtonForegroundColor)
}
}

View File

@ -402,26 +402,32 @@ extension PresentationThemeRootNavigationBar: Codable {
case segmentedFg
case segmentedText
case segmentedDivider
case clearButtonBackground
case clearButtonForeground
}
public convenience init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.init(buttonColor: try decodeColor(values, .button),
disabledButtonColor: try decodeColor(values, .disabledButton),
primaryTextColor: try decodeColor(values, .primaryText),
secondaryTextColor: try decodeColor(values, .secondaryText),
controlColor: try decodeColor(values, .control),
accentTextColor: try decodeColor(values, .accentText),
backgroundColor: try decodeColor(values, .background),
separatorColor: try decodeColor(values, .separator),
badgeBackgroundColor: try decodeColor(values, .badgeFill),
badgeStrokeColor: try decodeColor(values, .badgeStroke),
badgeTextColor: try decodeColor(values, .badgeText),
segmentedBackgroundColor: try decodeColor(values, .segmentedBg, decoder: decoder, fallbackKey: "root.searchBar.inputFill"),
segmentedForegroundColor: try decodeColor(values, .segmentedFg, decoder: decoder, fallbackKey: "root.navBar.background"),
segmentedTextColor: try decodeColor(values, .segmentedText, decoder: decoder, fallbackKey: "root.navBar.primaryText"),
segmentedDividerColor: try decodeColor(values, .segmentedDivider, decoder: decoder, fallbackKey: "root.list.freeInputField.stroke"))
self.init(
buttonColor: try decodeColor(values, .button),
disabledButtonColor: try decodeColor(values, .disabledButton),
primaryTextColor: try decodeColor(values, .primaryText),
secondaryTextColor: try decodeColor(values, .secondaryText),
controlColor: try decodeColor(values, .control),
accentTextColor: try decodeColor(values, .accentText),
backgroundColor: try decodeColor(values, .background),
separatorColor: try decodeColor(values, .separator),
badgeBackgroundColor: try decodeColor(values, .badgeFill),
badgeStrokeColor: try decodeColor(values, .badgeStroke),
badgeTextColor: try decodeColor(values, .badgeText),
segmentedBackgroundColor: try decodeColor(values, .segmentedBg, decoder: decoder, fallbackKey: "root.searchBar.inputFill"),
segmentedForegroundColor: try decodeColor(values, .segmentedFg, decoder: decoder, fallbackKey: "root.navBar.background"),
segmentedTextColor: try decodeColor(values, .segmentedText, decoder: decoder, fallbackKey: "root.navBar.primaryText"),
segmentedDividerColor: try decodeColor(values, .segmentedDivider, decoder: decoder, fallbackKey: "root.list.freeInputField.stroke"),
clearButtonBackgroundColor: try decodeColor(values, .clearButtonBackground, decoder: decoder, fallbackKey: "root.list.freeInputField.bg"),
clearButtonForegroundColor: try decodeColor(values, .clearButtonForeground, decoder: decoder, fallbackKey: "root.list.freeInputField.primary")
)
}
public func encode(to encoder: Encoder) throws {

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_reorder.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -61,7 +61,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
let sideInset: CGFloat = 13.0
let bottomInset: CGFloat = 10.0
let contentInset: CGFloat = 9.0
let contentVerticalInset: CGFloat = 9.0
let contentVerticalInset: CGFloat = 11.0
let animationSize = CGSize(width: 32.0, height: 32.0)
let animationInset: CGFloat = (70 - animationSize.width) / 2.0
let animationSpacing: CGFloat = 8.0