mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Update tab design
This commit is contained in:
parent
c969f2556d
commit
a29dc35c68
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
994
submodules/Postbox/Sources/ChatListViewState.swift
Normal file
994
submodules/Postbox/Sources/ChatListViewState.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_reorder.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/ic_reorder.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReorderItems.imageset/ic_reorder.pdf
vendored
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/ChatListNewFolder.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/ChatListNewFolder.tgs
Normal file
Binary file not shown.
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user