Filter improvements

This commit is contained in:
Ali 2020-02-21 18:33:15 +04:00
parent 868dcdf05d
commit 7db3e89a53
51 changed files with 4255 additions and 3163 deletions

View File

@ -5346,3 +5346,9 @@ Any member of this group will be able to see messages in the channel.";
"PeerInfo.BioExpand" = "more";
"External.OpenIn" = "Open in %@";
"ChatList.EmptyChatList" = "You have no\nconversations yet.";
"ChatList.EmptyChatFilterList" = "No chats currently\nmatch this filter.";
"ChatList.EmptyChatListNewMessage" = "New Message";
"ChatList.EmptyChatListEditFilter" = "Edit Filter";

View File

@ -43,6 +43,8 @@ static_library(
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/TelegramIntents:TelegramIntents",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -17,10 +17,10 @@ func archiveContextMenuItems(context: AccountContext, groupId: PeerGroupId, chat
return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in
var items: [ContextMenuItem] = []
if !transaction.getUnreadChatListPeerIds(groupId: groupId).isEmpty {
if !transaction.getUnreadChatListPeerIds(groupId: groupId, filterPredicate: nil).isEmpty {
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAllAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { [weak chatListController] _, f in
let _ = (context.account.postbox.transaction { transaction in
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId)
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: nil)
}
|> deliverOnMainQueue).start(completed: {
f(.default)

View File

@ -95,7 +95,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
}
}
public class ChatListControllerImpl: TelegramBaseController, ChatListController, UIViewControllerPreviewingDelegate, TabBarContainedController {
public class ChatListControllerImpl: TelegramBaseController, ChatListController, UIViewControllerPreviewingDelegate {
private var validLayout: ContainerViewLayout?
public let context: AccountContext
@ -166,6 +166,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary)
self.hasTabBarItemContextAction = true
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
let title: String
@ -410,9 +412,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
ApplicationSpecificPreferencesKeys.chatListFilterSettings
]))
let filterItems = chatListFilterItems(context: context)
|> map { items -> [ChatListFilterTabEntry] in
|> map { totalCount, items -> [ChatListFilterTabEntry] in
var result: [ChatListFilterTabEntry] = []
result.append(.all)
result.append(.all(unreadCount: totalCount))
for (filter, unreadCount) in items {
result.append(.filter(id: filter.id, text: filter.title ?? "", unreadCount: unreadCount))
}
@ -438,7 +440,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
var resolvedItems = filterItems
if !filterSettings.displayTabs {
if !filterSettings.displayTabs || groupId != .root {
resolvedItems = []
}
@ -483,28 +485,90 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
let previousFilter = strongSelf.chatListDisplayNode.chatListNode.chatListFilter
let updatedFilter: ChatListFilter?
switch id {
case .all:
strongSelf.chatListDisplayNode.chatListNode.chatListFilter = nil
updatedFilter = nil
case let .filter(id):
var found = false
var foundValue: ChatListFilter?
for filter in filters {
if filter.id == id {
strongSelf.chatListDisplayNode.chatListNode.chatListFilter = filter
foundValue = filter
found = true
break
}
}
if !found {
strongSelf.chatListDisplayNode.chatListNode.chatListFilter = nil
if found {
updatedFilter = foundValue
} else {
updatedFilter = nil
}
}
if previousFilter?.id != updatedFilter?.id {
var paneSwitchAnimationDirection: ChatListNodePaneSwitchAnimationDirection?
if let previousId = previousFilter?.id, let updatedId = updatedFilter?.id, let previousIndex = filters.index(where: { $0.id == previousId }), let updatedIndex = filters.index(where: { $0.id == updatedId }) {
if previousIndex > updatedIndex {
paneSwitchAnimationDirection = .right
} else {
paneSwitchAnimationDirection = .left
}
} else if (previousFilter != nil) != (updatedFilter != nil) {
if previousFilter != nil {
paneSwitchAnimationDirection = .right
} else {
paneSwitchAnimationDirection = .left
}
}
if let direction = paneSwitchAnimationDirection {
strongSelf.chatListDisplayNode.chatListNode.paneSwitchAnimation = (direction, .animated(duration: 0.4, curve: .spring))
}
}
strongSelf.chatListDisplayNode.chatListNode.updateFilter(updatedFilter)
})
}
self.tabContainerNode.addFilter = { [weak self] in
self?.openFilterSettings()
}
self.tabContainerNode.contextGesture = { [weak self] id, sourceNode, gesture in
guard let strongSelf = self else {
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Edit, icon: { _ in
return nil
}, action: { c, f in
c.dismiss(completion: {
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [ChatListFilter] in
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default
return settings.filters
}
|> deliverOnMainQueue).start(next: { presetList in
guard let strongSelf = self else {
return
}
var found = false
for filter in presetList {
if filter.id == id {
strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in }))
f(.dismissWithoutContent)
found = true
break
}
}
})
})
})))
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture)
strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
}
}
required public init(coder aDecoder: NSCoder) {
@ -570,7 +634,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
override public func loadDisplayNode() {
self.displayNode = ChatListControllerNode(context: self.context, groupId: self.groupId, filter: self.filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, controller: self)
self.chatListFilterValue.set(self.chatListDisplayNode.chatListNode.chatListFilterSignal)
self.chatListFilterValue.set(self.chatListDisplayNode.chatListNode.appliedChatListFilterSignal)
self.chatListDisplayNode.navigationBar = self.navigationBar
@ -776,11 +840,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.chatListDisplayNode.isEmptyUpdated = { [weak self] isEmpty in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode, let _ = strongSelf.validLayout {
if isEmpty {
searchContentNode.updateListVisibleContentOffset(.known(0.0))
//searchContentNode.updateListVisibleContentOffset(.known(0.0))
}
}
}
self.chatListDisplayNode.emptyListAction = { [weak self] in
guard let strongSelf = self else {
return
}
if let filter = strongSelf.chatListDisplayNode.chatListNode.chatListFilter {
strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in }))
} else {
strongSelf.composePressed()
}
}
self.chatListDisplayNode.toolbarActionSelected = { [weak self] action in
self?.toolbarActionSelected(action: action)
}
@ -1310,7 +1385,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} else {
let groupId = self.groupId
signal = self.context.account.postbox.transaction { transaction -> Void in
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId)
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: self.chatListDisplayNode.chatListNode.chatListFilter.flatMap(chatListFilterPredicate))
}
}
let _ = (signal
@ -1977,4 +2052,157 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
public func updateTabBarPreviewingControllerPresentation(_ update: TabBarContainedControllerPresentationUpdate) {
}
override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
let _ = (combineLatest(queue: .mainQueue(),
self.context.account.postbox.transaction { transaction -> [ChatListFilter] in
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default
return settings.filters
},
chatListFilterItems(context: self.context)
|> take(1)
)
|> deliverOnMainQueue).start(next: { [weak self] presetList, filterItemsAndTotalCount in
guard let strongSelf = self else {
return
}
let (_, filterItems) = filterItemsAndTotalCount
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: "Setup", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { c, f in
c.dismiss(completion: {
guard let strongSelf = self else {
return
}
strongSelf.push(chatListFilterPresetListController(context: strongSelf.context, updated: { _ in }))
})
})))
if !presetList.isEmpty {
items.append(.separator)
for preset in presetList {
enum ChatListFilterType {
case generic
case unmuted
case unread
case channels
case groups
case bots
case secretChats
case privateChats
}
let filterType: ChatListFilterType
if preset.includePeers.isEmpty {
if preset.categories == .all {
if preset.excludeRead {
filterType = .unread
} else if preset.excludeMuted {
filterType = .unmuted
} else {
filterType = .generic
}
} else {
if preset.categories == .channels {
filterType = .channels
} else if preset.categories.isSubset(of: [.publicGroups, .privateGroups]) {
filterType = .groups
} else if preset.categories == .bots {
filterType = .bots
} else if preset.categories == .secretChats {
filterType = .secretChats
} else if preset.categories == .privateChats {
filterType = .privateChats
} else {
filterType = .generic
}
}
} else {
filterType = .generic
}
var badge = ""
for item in filterItems {
if item.0.id == preset.id && item.1 != 0 {
badge = "\(item.1)"
}
}
items.append(.action(ContextMenuActionItem(text: preset.title ?? "", badge: badge, icon: { theme in
let imageName: String
switch filterType {
case .generic:
imageName = "Chat/Context Menu/List"
case .unmuted:
imageName = "Chat/Context Menu/Unmute"
case .unread:
imageName = "Chat/Context Menu/MarkAsUnread"
case .channels:
imageName = "Chat/Context Menu/Channels"
case .groups:
imageName = "Chat/Context Menu/Groups"
case .bots:
imageName = "Chat/Context Menu/Bots"
case .secretChats:
imageName = "Chat/Context Menu/Timer"
case .privateChats:
imageName = "Chat/Context Menu/User"
}
return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor)
}, action: { c, f in
c.dismiss(completion: {
guard let strongSelf = self else {
return
}
strongSelf.push(ChatListControllerImpl(context: strongSelf.context, groupId: .root, filter: preset, controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: false, enableDebugActions: false))
})
})))
}
}
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture)
strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
})
}
}
private final class ChatListTabBarContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = true
private let controller: ChatListController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ChatListController, sourceNode: ContextExtractedContentContainingNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
private final class ChatListHeaderBarContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = false
private let controller: ChatListController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ChatListController, sourceNode: ContextExtractedContentContainingNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View File

@ -47,7 +47,7 @@ final class ChatListControllerNode: ASDisplayNode {
private let groupId: PeerGroupId
private var presentationData: PresentationData
private var chatListEmptyNode: ChatListEmptyNode?
private var chatListEmptyNodeContainer: ChatListEmptyNodeContainer
private var chatListEmptyIndicator: ActivityIndicator?
let chatListNode: ChatListNode
var navigationBar: NavigationBar?
@ -69,6 +69,7 @@ final class ChatListControllerNode: ASDisplayNode {
var peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?
var dismissSelf: (() -> Void)?
var isEmptyUpdated: ((Bool) -> Void)?
var emptyListAction: (() -> Void)?
let debugListView = ListView()
@ -77,6 +78,7 @@ final class ChatListControllerNode: ASDisplayNode {
self.groupId = groupId
self.presentationData = presentationData
self.chatListEmptyNodeContainer = ChatListEmptyNodeContainer(theme: presentationData.theme, strings: presentationData.strings)
self.chatListNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
self.controller = controller
@ -90,51 +92,33 @@ final class ChatListControllerNode: ASDisplayNode {
self.backgroundColor = presentationData.theme.chatList.backgroundColor
self.addSubnode(self.chatListNode)
self.chatListNode.isEmptyUpdated = { [weak self] isEmptyState in
self.addSubnode(self.chatListEmptyNodeContainer)
self.chatListNode.isEmptyUpdated = { [weak self] isEmptyState, isFilter, transitionDirection, transition in
guard let strongSelf = self else {
return
}
switch isEmptyState {
case .empty(false):
if case .group = strongSelf.groupId {
strongSelf.dismissSelf?()
} else if strongSelf.chatListEmptyNode == nil {
let chatListEmptyNode = ChatListEmptyNode(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings)
strongSelf.chatListEmptyNode = chatListEmptyNode
strongSelf.insertSubnode(chatListEmptyNode, belowSubnode: strongSelf.chatListNode)
if let (layout, navigationHeight, visualNavigationHeight, cleanNavigationBarHeight) = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
}
strongSelf.isEmptyUpdated?(true)
}
case .notEmpty(false):
if case .group = strongSelf.groupId {
strongSelf.dismissSelf?()
}
default:
if let chatListEmptyNode = strongSelf.chatListEmptyNode {
strongSelf.chatListEmptyNode = nil
chatListEmptyNode.removeFromSupernode()
}
}
switch isEmptyState {
case .empty(true):
if strongSelf.chatListEmptyIndicator == nil {
let chatListEmptyIndicator = ActivityIndicator(type: .custom(strongSelf.presentationData.theme.list.itemSecondaryTextColor, 22.0, 1.0, false))
strongSelf.chatListEmptyIndicator = chatListEmptyIndicator
strongSelf.insertSubnode(chatListEmptyIndicator, belowSubnode: strongSelf.chatListNode)
if let (layout, navigationHeight, visualNavigationHeight, cleanNavigationBarHeight) = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
}
}
default:
if let chatListEmptyIndicator = strongSelf.chatListEmptyIndicator {
strongSelf.chatListEmptyIndicator = nil
chatListEmptyIndicator.removeFromSupernode()
}
case .empty(false):
if case .group = strongSelf.groupId {
strongSelf.dismissSelf?()
} else {
strongSelf.chatListEmptyNodeContainer.update(state: isEmptyState, isFilter: isFilter, direction: transitionDirection, transition: transition)
}
case .notEmpty(false):
if case .group = strongSelf.groupId {
strongSelf.dismissSelf?()
} else {
strongSelf.chatListEmptyNodeContainer.update(state: isEmptyState, isFilter: isFilter, direction: transitionDirection, transition: transition)
}
default:
strongSelf.chatListEmptyNodeContainer.update(state: isEmptyState, isFilter: isFilter, direction: transitionDirection, transition: transition)
}
}
self.chatListEmptyNodeContainer.action = { [weak self] in
self?.emptyListAction?()
}
self.addSubnode(self.debugListView)
}
@ -151,7 +135,7 @@ final class ChatListControllerNode: ASDisplayNode {
self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)
self.searchDisplayController?.updatePresentationData(presentationData)
self.chatListEmptyNode?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings)
self.chatListEmptyNodeContainer.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings)
if let toolbarNode = self.toolbarNode {
toolbarNode.updateTheme(TabBarControllerTheme(rootControllerTheme: self.presentationData.theme))
@ -219,11 +203,9 @@ final class ChatListControllerNode: ASDisplayNode {
self.chatListNode.visualInsets = UIEdgeInsets(top: visualNavigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
if let chatListEmptyNode = self.chatListEmptyNode {
let emptySize = CGSize(width: updateSizeAndInsets.size.width, height: updateSizeAndInsets.size.height - updateSizeAndInsets.insets.top - updateSizeAndInsets.insets.bottom)
transition.updateFrame(node: chatListEmptyNode, frame: CGRect(origin: CGPoint(x: 0.0, y: updateSizeAndInsets.insets.top), size: emptySize))
chatListEmptyNode.updateLayout(size: emptySize, transition: transition)
}
let emptySize = CGSize(width: updateSizeAndInsets.size.width, height: updateSizeAndInsets.size.height - updateSizeAndInsets.insets.top - updateSizeAndInsets.insets.bottom)
transition.updateFrame(node: self.chatListEmptyNodeContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: updateSizeAndInsets.insets.top), size: emptySize))
self.chatListEmptyNodeContainer.updateLayout(size: emptySize, transition: transition)
if let chatListEmptyIndicator = self.chatListEmptyIndicator {
let indicatorSize = chatListEmptyIndicator.measure(CGSize(width: 100.0, height: 100.0))

View File

@ -3,13 +3,147 @@ import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AnimatedStickerNode
import AppBundle
import SolidRoundedButtonNode
final class ChatListEmptyNodeContainer: ASDisplayNode {
private var currentNode: ChatListEmptyNode?
private var theme: PresentationTheme
private var strings: PresentationStrings
private var validLayout: CGSize?
var action: (() -> Void)?
init(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
super.init()
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
if let currentNode = self.currentNode {
currentNode.updateThemeAndStrings(theme: theme, strings: strings)
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
if let currentNode = self.currentNode {
currentNode.updateLayout(size: size, transition: transition)
}
}
func update(state: ChatListNodeEmptyState, isFilter: Bool, direction: ChatListNodePaneSwitchAnimationDirection?, transition: ContainedViewLayoutTransition) {
switch state {
case .empty:
if let direction = direction {
let previousNode = self.currentNode
let currentNode = ChatListEmptyNode(isFilter: isFilter, theme: self.theme, strings: self.strings, action: { [weak self] in
self?.action?()
})
self.currentNode = currentNode
if let size = self.validLayout {
currentNode.frame = CGRect(origin: CGPoint(), size: size)
currentNode.updateLayout(size: size, transition: .immediate)
}
self.addSubnode(currentNode)
if case .animated = transition, let size = self.validLayout {
let offset: CGFloat
switch direction {
case .left:
offset = -size.width
case .right:
offset = size.width
}
if let previousNode = previousNode {
previousNode.frame = self.bounds.offsetBy(dx: offset, dy: 0.0)
}
transition.animateHorizontalOffsetAdditive(node: self, offset: offset, completion: { [weak previousNode] in
previousNode?.removeFromSupernode()
})
} else {
previousNode?.removeFromSupernode()
}
} else {
if let previousNode = self.currentNode, previousNode.isFilter != isFilter {
let currentNode = ChatListEmptyNode(isFilter: isFilter, theme: self.theme, strings: self.strings, action: { [weak self] in
self?.action?()
})
self.currentNode = currentNode
if let size = self.validLayout {
currentNode.frame = CGRect(origin: CGPoint(), size: size)
currentNode.updateLayout(size: size, transition: .immediate)
}
self.addSubnode(currentNode)
previousNode.removeFromSupernode()
} else if self.currentNode == nil {
let currentNode = ChatListEmptyNode(isFilter: isFilter, theme: self.theme, strings: self.strings, action: { [weak self] in
self?.action?()
})
self.currentNode = currentNode
if let size = self.validLayout {
currentNode.frame = CGRect(origin: CGPoint(), size: size)
currentNode.updateLayout(size: size, transition: .immediate)
}
self.addSubnode(currentNode)
}
}
case .notEmpty:
if let previousNode = self.currentNode {
self.currentNode = nil
if let direction = direction {
if case .animated = transition, let size = self.validLayout {
let offset: CGFloat
switch direction {
case .left:
offset = -size.width
case .right:
offset = size.width
}
previousNode.frame = self.bounds.offsetBy(dx: offset, dy: 0.0)
transition.animateHorizontalOffsetAdditive(node: self, offset: offset, completion: { [weak previousNode] in
previousNode?.removeFromSupernode()
})
} else {
previousNode.removeFromSupernode()
}
} else {
previousNode.removeFromSupernode()
}
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let currentNode = self.currentNode {
return currentNode.view.hitTest(self.view.convert(point, to: currentNode.view), with: event)
}
return nil
}
}
final class ChatListEmptyNode: ASDisplayNode {
let isFilter: Bool
private let textNode: ImmediateTextNode
private let animationNode: AnimatedStickerNode
private let buttonNode: SolidRoundedButtonNode
private var animationSize: CGSize = CGSize()
private var validLayout: CGSize?
init(theme: PresentationTheme, strings: PresentationStrings) {
init(isFilter: Bool, theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) {
self.isFilter = isFilter
self.animationNode = AnimatedStickerNode()
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
@ -17,19 +151,39 @@ final class ChatListEmptyNode: ASDisplayNode {
self.textNode.textAlignment = .center
self.textNode.lineSpacing = 0.1
self.buttonNode = SolidRoundedButtonNode(title: isFilter ? strings.ChatList_EmptyChatListEditFilter : strings.ChatList_EmptyChatListNewMessage, theme: SolidRoundedButtonTheme(backgroundColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 10.0, gloss: false)
super.init()
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
self.addSubnode(self.buttonNode)
let animationName: String
if isFilter {
animationName = "ChatListFilterEmpty"
} else {
animationName = "ChatListEmpty"
}
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct)
self.animationSize = CGSize(width: 124.0, height: 124.0)
self.animationNode.visibility = true
}
self.buttonNode.pressed = {
action()
}
self.updateThemeAndStrings(theme: theme, strings: strings)
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
let string = NSMutableAttributedString()
string.append(NSAttributedString(string: strings.DialogList_NoMessagesTitle + "\n", font: Font.medium(17.0), textColor: theme.list.itemSecondaryTextColor, paragraphAlignment: .center))
string.append(NSAttributedString(string: strings.DialogList_NoMessagesText, font: Font.regular(16.0), textColor: theme.list.itemSecondaryTextColor, paragraphAlignment: .center))
let string = NSMutableAttributedString(string: self.isFilter ? strings.ChatList_EmptyChatFilterList : strings.ChatList_EmptyChatList, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor)
self.textNode.attributedText = string
self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor))
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
@ -38,7 +192,44 @@ final class ChatListEmptyNode: ASDisplayNode {
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
let animationSpacing: CGFloat = 10.0
let buttonSpacing: CGFloat = 24.0
let buttonSideInset: CGFloat = 16.0
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 40.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize))
let buttonWidth = min(size.width - buttonSideInset * 2.0, 280.0)
let buttonSize = CGSize(width: buttonWidth, height: 50.0)
let contentHeight = self.animationSize.height + animationSpacing + textSize.height + buttonSpacing + buttonSize.height
var contentOffset: CGFloat = 0.0
if size.height < contentHeight {
contentOffset = -self.animationSize.height - animationSpacing + 44.0
transition.updateAlpha(node: self.animationNode, alpha: 0.0)
} else {
contentOffset = -40.0
transition.updateAlpha(node: self.animationNode, alpha: 1.0)
}
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - self.animationSize.width) / 2.0), y: floor((size.height - contentHeight) / 2.0) + contentOffset), size: self.animationSize)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: animationFrame.maxY + animationSpacing), size: textSize)
let buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: textFrame.maxY + buttonSpacing), size: buttonSize)
if !self.animationSize.width.isZero {
self.animationNode.updateLayout(size: self.animationSize)
transition.updateFrame(node: self.animationNode, frame: animationFrame)
}
transition.updateFrame(node: self.textNode, frame: textFrame)
self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.buttonNode.frame.contains(point) {
return self.buttonNode.view.hitTest(self.view.convert(point, to: self.buttonNode.view), with: event)
}
return nil
}
}

View File

@ -371,7 +371,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
let rightNavigationButton = ItemListNavigationButton(content: .text("Create"), style: .bold, enabled: state.isComplete, action: {
let rightNavigationButton = ItemListNavigationButton(content: .text(currentPreset == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: {
let state = stateValue.with { $0 }
let preset = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, includePeers: state.additionallyIncludePeers)
let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in

View File

@ -10,22 +10,36 @@ import TelegramPresentationData
private final class ItemNode: ASDisplayNode {
private let pressed: () -> Void
private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let titleNode: ImmediateTextNode
private let extractedTitleNode: ImmediateTextNode
private let badgeContainerNode: ASDisplayNode
private let badgeTextNode: ImmediateTextNode
private let badgeBackgroundNode: ASImageNode
private let buttonNode: HighlightTrackingButtonNode
private var isSelected: Bool = false
private var unreadCount: Int = 0
private(set) var unreadCount: Int = 0
private var theme: PresentationTheme?
init(pressed: @escaping () -> Void) {
init(pressed: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) {
self.pressed = pressed
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.extractedTitleNode = ImmediateTextNode()
self.extractedTitleNode.displaysAsynchronously = false
self.extractedTitleNode.alpha = 0.0
self.badgeContainerNode = ASDisplayNode()
self.badgeTextNode = ImmediateTextNode()
self.badgeTextNode.displaysAsynchronously = false
@ -37,57 +51,101 @@ private final class ItemNode: ASDisplayNode {
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.badgeBackgroundNode)
self.addSubnode(self.badgeTextNode)
self.addSubnode(self.buttonNode)
self.extractedContainerNode.contentNode.addSubnode(self.titleNode)
self.extractedContainerNode.contentNode.addSubnode(self.extractedTitleNode)
self.badgeContainerNode.addSubnode(self.badgeBackgroundNode)
self.badgeContainerNode.addSubnode(self.badgeTextNode)
self.extractedContainerNode.contentNode.addSubnode(self.badgeContainerNode)
self.extractedContainerNode.contentNode.addSubnode(self.buttonNode)
self.containerNode.addSubnode(self.extractedContainerNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addSubnode(self.containerNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.containerNode.activated = { [weak self] gesture in
guard let strongSelf = self else {
return
}
contextGesture(strongSelf.extractedContainerNode, gesture)
}
self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
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)
}
}
@objc private func buttonPressed() {
self.pressed()
}
func updateText(title: String, unreadCount: Int, isSelected: Bool, presentationData: PresentationData) {
func updateText(title: String, unreadCount: Int, isNoFilter: Bool, isSelected: Bool, presentationData: PresentationData) {
if self.theme !== presentationData.theme {
self.theme = presentationData.theme
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.rootController.navigationBar.badgeBackgroundColor)
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.list.itemCheckColors.fillColor)
}
self.containerNode.isGestureEnabled = !isNoFilter
self.isSelected = isSelected
self.unreadCount = unreadCount
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)
if unreadCount != 0 {
self.badgeTextNode.attributedText = NSAttributedString(string: "\(unreadCount)", font: Font.regular(14.0), textColor: presentationData.theme.rootController.navigationBar.badgeTextColor)
self.badgeTextNode.attributedText = NSAttributedString(string: "\(unreadCount)", font: Font.regular(14.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
}
}
func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> 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 badgeSize = self.badgeTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
let badgeInset: CGFloat = 5.0
let badgeInset: CGFloat = 4.0
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: titleSize.width + 5.0, y: floor((height - 18.0) / 2.0)), size: CGSize(width: max(18.0, badgeSize.width + badgeInset * 2.0), height: 18.0))
self.badgeBackgroundNode.frame = badgeBackgroundFrame
self.badgeTextNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeSize.width) / 2.0), y: badgeBackgroundFrame.minY + floor((badgeBackgroundFrame.height - badgeSize.height) / 2.0)), size: badgeSize)
self.badgeContainerNode.frame = badgeBackgroundFrame
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.badgeBackgroundNode.alpha = 0.0
self.badgeTextNode.alpha = 0.0
self.badgeContainerNode.alpha = 0.0
return titleSize.width
} else {
self.badgeBackgroundNode.alpha = 1.0
self.badgeTextNode.alpha = 1.0
self.badgeContainerNode.alpha = 1.0
return badgeBackgroundFrame.maxX
}
}
func updateArea(size: CGSize, sideInset: CGFloat) {
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.containerNode.frame = CGRect(origin: CGPoint(), size: size)
}
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)
}
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)
}
}
@ -97,7 +155,7 @@ enum ChatListFilterTabEntryId: Hashable {
}
enum ChatListFilterTabEntry: Equatable {
case all
case all(unreadCount: Int)
case filter(id: Int32, text: String, unreadCount: Int)
var id: ChatListFilterTabEntryId {
@ -162,6 +220,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
var tabSelected: ((ChatListFilterTabEntryId) -> Void)?
var addFilter: (() -> Void)?
var contextGesture: ((Int32, ContextExtractedContentContainingNode, ContextGesture) -> Void)?
private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, presentationData: PresentationData)?
@ -208,6 +267,13 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
enum BadgeAnimation {
case `in`
case out
}
var badgeAnimations: [ChatListFilterTabEntryId: BadgeAnimation] = [:]
for filter in filters {
let itemNode: ItemNode
var wasAdded = false
@ -217,17 +283,35 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
wasAdded = true
itemNode = ItemNode(pressed: { [weak self] in
self?.tabSelected?(filter.id)
}, contextGesture: { [weak self] sourceNode, gesture in
guard let strongSelf = self else {
return
}
switch filter {
case let .filter(filter):
strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = false
strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = true
strongSelf.scrollNode.view.setContentOffset(strongSelf.scrollNode.view.contentOffset, animated: false)
strongSelf.contextGesture?(filter.id, sourceNode, gesture)
default:
break
}
})
self.itemNodes[filter.id] = itemNode
}
let unreadCount: Int
var isNoFilter: Bool = false
switch filter {
case .all:
unreadCount = 0
case let .all(count):
unreadCount = count
isNoFilter = true
case let .filter(filter):
unreadCount = filter.unreadCount
}
itemNode.updateText(title: filter.title(strings: presentationData.strings), unreadCount: unreadCount, isSelected: selectedFilter == filter.id, presentationData: presentationData)
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)
}
var removeKeys: [ChatListFilterTabEntryId] = []
for (id, _) in self.itemNodes {
@ -257,6 +341,15 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height)
tabSizes.append((paneNodeSize, itemNode, wasAdded))
totalRawTabSize += paneNodeSize.width
if case .animated = transition, let badgeAnimation = badgeAnimations[filter.id] {
switch badgeAnimation {
case .in:
itemNode.animateBadgeIn()
case .out:
itemNode.animateBadgeOut()
}
}
}
let minSpacing: CGFloat = 30.0
@ -271,7 +364,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
paneNode.alpha = 0.0
transition.updateAlpha(node: paneNode, alpha: 1.0)
} else {
transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame)
transition.updateFrameAdditive(node: paneNode, frame: paneFrame)
}
paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0)
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing / 2.0, bottom: 0.0, right: -minSpacing / 2.0)
@ -286,7 +379,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
self.addNode.update(size: addSize, theme: presentationData.theme)
leftOffset += addSize.width + minSpacing
self.scrollNode.view.contentSize = CGSize(width: leftOffset - minSpacing + sideInset, height: size.height)
self.scrollNode.view.contentSize = CGSize(width: leftOffset - minSpacing + sideInset - 5.0, height: size.height)
let transitionFraction: CGFloat = 0.0
var selectedFrame: CGRect?

View File

@ -0,0 +1,82 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramPresentationData
import ListSectionHeaderNode
import AppBundle
class ChatListEmptyHeaderItem: ListViewItem {
let selectable: Bool = false
init() {
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatListEmptyHeaderItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply()
})
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ChatListEmptyHeaderItemNode)
if let nodeValue = node() as? ChatListEmptyHeaderItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem == nil)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
class ChatListEmptyHeaderItemNode: ListViewItemNode {
private var item: ChatListEmptyHeaderItem?
required init() {
super.init(layerBacked: false, dynamicBounce: false)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListEmptyHeaderItem, params, nextItem == nil)
apply()
}
func asyncLayout() -> (_ item: ChatListEmptyHeaderItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, last in
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 0.0), insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
}
})
}
}
}

View File

@ -145,6 +145,8 @@ public struct ChatListNodeState: Equatable {
private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
return entries.map { entry -> ListViewInsertItem in
switch entry.entry {
case .HeaderEntry:
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd, hasFailedMessages):
switch mode {
case .chatList:
@ -278,6 +280,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, index: index, content: .groupReference(groupId: groupId, peers: peers, message: message, unreadState: unreadState, hiddenByDefault: hiddenByDefault), editing: editing, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: true, hiddenOffset: hiddenByDefault && !revealed, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .ArchiveIntro(presentationData):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
case .HeaderEntry:
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
}
}
}
@ -323,6 +327,11 @@ public enum ChatListNodeEmptyState: Equatable {
case empty(isLoading: Bool)
}
enum ChatListNodePaneSwitchAnimationDirection {
case left
case right
}
public final class ChatListNode: ListView {
private let controlsHistoryPreload: Bool
private let context: AccountContext
@ -367,8 +376,9 @@ public final class ChatListNode: ListView {
return self.statePromise.get()
}
var paneSwitchAnimation: (ChatListNodePaneSwitchAnimationDirection, ContainedViewLayoutTransition)?
private var currentLocation: ChatListNodeLocation?
var chatListFilter: ChatListFilter? {
private(set) var chatListFilter: ChatListFilter? {
didSet {
self.chatListFilterValue.set(.single(self.chatListFilter))
@ -379,10 +389,17 @@ public final class ChatListNode: ListView {
}
}
}
private let updatedFilterDisposable = MetaDisposable()
private let chatListFilterValue = Promise<ChatListFilter?>()
var chatListFilterSignal: Signal<ChatListFilter?, NoError> {
return self.chatListFilterValue.get()
}
private var hasUpdatedAppliedChatListFilterValueOnce = false
private var currentAppliedChatListFilterValue: ChatListFilter?
private let appliedChatListFilterValue = Promise<ChatListFilter?>()
var appliedChatListFilterSignal: Signal<ChatListFilter?, NoError> {
return self.appliedChatListFilterValue.get()
}
private let chatListLocation = ValuePromise<ChatListNodeLocation>()
private let chatListDisposable = MetaDisposable()
private var activityStatusesDisposable: Disposable?
@ -413,7 +430,7 @@ public final class ChatListNode: ListView {
}
}
public var isEmptyUpdated: ((ChatListNodeEmptyState) -> Void)?
var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ChatListNodePaneSwitchAnimationDirection?, ContainedViewLayoutTransition) -> Void)?
private var currentIsEmptyState: ChatListNodeEmptyState?
public var addedVisibleChatsWithPeerIds: (([PeerId]) -> Void)?
@ -1138,11 +1155,50 @@ public final class ChatListNode: ListView {
}
}
}
self.resetFilter()
}
deinit {
self.chatListDisposable.dispose()
self.activityStatusesDisposable?.dispose()
self.updatedFilterDisposable.dispose()
}
func updateFilter(_ filter: ChatListFilter?) {
if filter?.id != self.chatListFilter?.id {
self.chatListFilter = filter
self.resetFilter()
}
}
private func resetFilter() {
if let chatListFilter = chatListFilter {
let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.chatListFilters]))
self.updatedFilterDisposable.set((context.account.postbox.combinedView(keys: [preferencesKey])
|> map { view -> ChatListFilter? in
guard let preferencesView = view.views[preferencesKey] as? PreferencesView else {
return nil
}
let filersState = preferencesView.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState ?? ChatListFiltersState.default
for filter in filersState.filters {
if filter.id == chatListFilter.id {
return filter
}
}
return nil
}
|> deliverOnMainQueue).start(next: { [weak self] updatedFilter in
guard let strongSelf = self else {
return
}
if strongSelf.chatListFilter != updatedFilter {
strongSelf.chatListFilter = updatedFilter
}
}))
} else {
self.updatedFilterDisposable.set(nil)
}
}
public func updateThemeAndStrings(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) {
@ -1200,6 +1256,13 @@ public final class ChatListNode: ListView {
if let (transition, completion) = self.enqueuedTransition {
self.enqueuedTransition = nil
let paneSwitchCopyView: UIView?
if let (direction, transition) = self.paneSwitchAnimation, let copyView = self.view.snapshotContentTree(unhide: false, keepTransform: true) {
paneSwitchCopyView = copyView
} else {
paneSwitchCopyView = nil
}
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
if let strongSelf = self {
strongSelf.chatListView = transition.chatListView
@ -1241,9 +1304,16 @@ public final class ChatListNode: ListView {
if transition.chatListView.filteredEntries.isEmpty {
isEmpty = true
} else {
if transition.chatListView.filteredEntries.count == 1 {
if case .GroupReferenceEntry = transition.chatListView.filteredEntries[0] {
isEmpty = true
if transition.chatListView.filteredEntries.count <= 2 {
isEmpty = true
loop: for entry in transition.chatListView.filteredEntries {
switch entry {
case .GroupReferenceEntry, .HeaderEntry:
break
default:
isEmpty = false
break loop
}
}
}
}
@ -1260,16 +1330,12 @@ public final class ChatListNode: ListView {
case .GroupReferenceEntry, .HoleEntry, .PeerEntry:
containsChats = true
break loop
case .ArchiveIntro:
case .ArchiveIntro, .HeaderEntry:
break
}
}
isEmptyState = .notEmpty(containsChats: containsChats)
}
if strongSelf.currentIsEmptyState != isEmptyState {
strongSelf.currentIsEmptyState = isEmptyState
strongSelf.isEmptyUpdated?(isEmptyState)
}
var insertedPeerIds: [PeerId] = []
for item in transition.insertItems {
@ -1286,6 +1352,36 @@ public final class ChatListNode: ListView {
strongSelf.addedVisibleChatsWithPeerIds?(insertedPeerIds)
}
var isEmptyUpdate: (ChatListNodePaneSwitchAnimationDirection?, ContainedViewLayoutTransition) = (nil, .immediate)
if let (direction, transition) = strongSelf.paneSwitchAnimation {
strongSelf.paneSwitchAnimation = nil
if let copyView = paneSwitchCopyView {
let offset: CGFloat
switch direction {
case .left:
offset = -strongSelf.bounds.width
case .right:
offset = strongSelf.bounds.width
}
copyView.frame = strongSelf.bounds.offsetBy(dx: offset, dy: 0.0)
strongSelf.view.addSubview(copyView)
transition.animateHorizontalOffsetAdditive(node: strongSelf, offset: offset, completion: { [weak copyView] in
copyView?.removeFromSuperview()
})
isEmptyUpdate = (direction, transition)
}
}
if strongSelf.currentIsEmptyState != isEmptyState {
strongSelf.currentIsEmptyState = isEmptyState
strongSelf.isEmptyUpdated?(isEmptyState, transition.chatListView.filter != nil, isEmptyUpdate.0, isEmptyUpdate.1)
}
if !strongSelf.hasUpdatedAppliedChatListFilterValueOnce || transition.chatListView.filter != strongSelf.currentAppliedChatListFilterValue {
strongSelf.currentAppliedChatListFilterValue = transition.chatListView.filter
strongSelf.appliedChatListFilterValue.set(.single(transition.chatListView.filter))
}
completion()
}
}

View File

@ -6,6 +6,7 @@ import TelegramPresentationData
import MergeLists
enum ChatListNodeEntryId: Hashable {
case Header
case Hole(Int64)
case PeerId(Int64)
case GroupId(PeerGroupId)
@ -13,6 +14,7 @@ enum ChatListNodeEntryId: Hashable {
}
enum ChatListNodeEntry: Comparable, Identifiable {
case HeaderEntry
case PeerEntry(index: ChatListIndex, presentationData: ChatListPresentationData, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, presence: PeerPresence?, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool, selected: Bool, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool, hasFailedMessages: Bool)
case HoleEntry(ChatListHole, theme: PresentationTheme)
case GroupReferenceEntry(index: ChatListIndex, presentationData: ChatListPresentationData, groupId: PeerGroupId, peers: [ChatListGroupReferencePeer], message: Message?, editing: Bool, unreadState: PeerGroupUnreadCountersCombinedSummary, revealed: Bool, hiddenByDefault: Bool)
@ -20,27 +22,31 @@ enum ChatListNodeEntry: Comparable, Identifiable {
var sortIndex: ChatListIndex {
switch self {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return index
case let .HoleEntry(hole, _):
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
case let .GroupReferenceEntry(index, _, _, _, _, _, _, _, _):
return index
case .ArchiveIntro:
return ChatListIndex.absoluteUpperBound
case .HeaderEntry:
return ChatListIndex.absoluteUpperBound
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return index
case let .HoleEntry(hole, _):
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
case let .GroupReferenceEntry(index, _, _, _, _, _, _, _, _):
return index
case .ArchiveIntro:
return ChatListIndex.absoluteUpperBound.successor
}
}
var stableId: ChatListNodeEntryId {
switch self {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return .PeerId(index.messageIndex.id.peerId.toInt64())
case let .HoleEntry(hole, _):
return .Hole(Int64(hole.index.id.id))
case let .GroupReferenceEntry(_, _, groupId, _, _, _, _, _, _):
return .GroupId(groupId)
case .ArchiveIntro:
return .ArchiveIntro
case .HeaderEntry:
return .Header
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return .PeerId(index.messageIndex.id.peerId.toInt64())
case let .HoleEntry(hole, _):
return .Hole(Int64(hole.index.id.id))
case let .GroupReferenceEntry(_, _, groupId, _, _, _, _, _, _):
return .GroupId(groupId)
case .ArchiveIntro:
return .ArchiveIntro
}
}
@ -50,6 +56,12 @@ enum ChatListNodeEntry: Comparable, Identifiable {
static func ==(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool {
switch lhs {
case .HeaderEntry:
if case .HeaderEntry = rhs {
return true
} else {
return false
}
case let .PeerEntry(lhsIndex, lhsPresentationData, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsSelected, lhsInputActivities, lhsAd, lhsHasFailedMessages):
switch rhs {
case let .PeerEntry(rhsIndex, rhsPresentationData, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsSelected, rhsInputActivities, rhsAd, rhsHasFailedMessages):
@ -273,6 +285,8 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
if displayArchiveIntro {
result.append(.ArchiveIntro(presentationData: state.presentationData))
}
result.append(.HeaderEntry)
}
}

View File

@ -29,79 +29,78 @@ struct ChatListNodeViewUpdate {
let scrollPosition: ChatListNodeViewScrollPosition?
}
func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> {
let filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?
if let filter = location.filter {
let includePeers = Set(filter.includePeers)
filterPredicate = { peer, notificationSettings, isUnread in
if includePeers.contains(peer.id) {
return true
}
if filter.excludeRead {
if !isUnread {
return false
}
}
if filter.excludeMuted {
if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
if case .muted = notificationSettings.muteState {
return false
}
} else {
return false
}
}
if !filter.categories.contains(.privateChats) {
if let user = peer as? TelegramUser {
if user.botInfo == nil {
return false
}
}
}
if !filter.categories.contains(.secretChats) {
if let _ = peer as? TelegramSecretChat {
return false
}
}
if !filter.categories.contains(.bots) {
if let user = peer as? TelegramUser {
if user.botInfo != nil {
return false
}
}
}
if !filter.categories.contains(.privateGroups) {
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(.publicGroups) {
if let channel = peer as? TelegramChannel {
if case .group = channel.info {
if channel.username != nil {
return false
}
}
}
}
if !filter.categories.contains(.channels) {
if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
return false
}
}
}
func chatListFilterPredicate(filter: ChatListFilter) -> (Peer, PeerNotificationSettings?, Bool) -> Bool {
let includePeers = Set(filter.includePeers)
return { peer, notificationSettings, isUnread in
if includePeers.contains(peer.id) {
return true
}
} else {
filterPredicate = nil
if filter.excludeRead {
if !isUnread {
return false
}
}
if filter.excludeMuted {
if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
if case .muted = notificationSettings.muteState {
return false
}
} else {
return false
}
}
if !filter.categories.contains(.privateChats) {
if let user = peer as? TelegramUser {
if user.botInfo == nil {
return false
}
}
}
if !filter.categories.contains(.secretChats) {
if let _ = peer as? TelegramSecretChat {
return false
}
}
if !filter.categories.contains(.bots) {
if let user = peer as? TelegramUser {
if user.botInfo != nil {
return false
}
}
}
if !filter.categories.contains(.privateGroups) {
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(.publicGroups) {
if let channel = peer as? TelegramChannel {
if case .group = channel.info {
if channel.username != nil {
return false
}
}
}
}
if !filter.categories.contains(.channels) {
if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
return false
}
}
}
return true
}
}
func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> {
let filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? = location.filter.flatMap(chatListFilterPredicate)
switch location {
case let .initial(count, _):

View File

@ -304,7 +304,7 @@ private final class FilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterI
}
}
func chatListFilterItems(context: AccountContext) -> Signal<[(ChatListFilter, Int)], NoError> {
func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilter, Int)]), NoError> {
let preferencesKey: PostboxViewKey = .preferences(keys: [PreferencesKeys.chatListFilters])
return context.account.postbox.combinedView(keys: [preferencesKey])
|> map { combinedView -> [ChatListFilter] in
@ -315,7 +315,7 @@ func chatListFilterItems(context: AccountContext) -> Signal<[(ChatListFilter, In
}
}
|> distinctUntilChanged
|> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in
|> mapToSignal { filters -> Signal<(Int, [(ChatListFilter, Int)]), NoError> in
var unreadCountItems: [UnreadMessageCountsItem] = []
unreadCountItems.append(.total(nil))
var additionalPeerIds = Set<PeerId>()
@ -334,10 +334,25 @@ func chatListFilterItems(context: AccountContext) -> Signal<[(ChatListFilter, In
keys.append(.basicPeer(peerId))
}
return context.account.postbox.combinedView(keys: keys)
|> map { view -> [(ChatListFilter, Int)] in
return combineLatest(queue: context.account.postbox.queue,
context.account.postbox.combinedView(keys: keys),
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings])
)
|> map { view, sharedData -> (Int, [(ChatListFilter, Int)]) in
guard let unreadCounts = view.views[unreadKey] as? UnreadMessageCountsView else {
return []
return (0, [])
}
let inAppSettings: InAppNotificationSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings {
inAppSettings = value
} else {
inAppSettings = .defaultSettings
}
let type: RenderedTotalUnreadCountType
switch inAppSettings.totalUnreadCountDisplayStyle {
case .filtered:
type = .filtered
}
var result: [(ChatListFilter, Int)] = []
@ -367,11 +382,9 @@ func chatListFilterItems(context: AccountContext) -> Signal<[(ChatListFilter, In
}
}
var totalUnreadChatCount = 0
var totalBadge = 0
if let totalState = totalState {
for (_, counters) in totalState.filteredCounters {
totalUnreadChatCount += Int(counters.chatCount)
}
totalBadge = Int(totalState.count(for: inAppSettings.totalUnreadCountDisplayStyle.category, in: inAppSettings.totalUnreadCountDisplayCategory.statsType, with: inAppSettings.totalUnreadCountIncludeTags))
}
var shouldUpdateLayout = false
@ -416,7 +429,7 @@ func chatListFilterItems(context: AccountContext) -> Signal<[(ChatListFilter, In
result.append((filter, count))
}
return result
return (totalBadge, result)
}
}
}

View File

@ -2,6 +2,7 @@ import Foundation
import AsyncDisplayKit
import Display
import TelegramPresentationData
import SwiftSignalKit
enum ContextActionSibling {
case none
@ -19,8 +20,12 @@ final class ContextActionNode: ASDisplayNode {
private let textNode: ImmediateTextNode
private let statusNode: ImmediateTextNode?
private let iconNode: ASImageNode
private let badgeBackgroundNode: ASImageNode
private let badgeTextNode: ImmediateTextNode
private let buttonNode: HighlightTrackingButtonNode
private var iconDisposable: Disposable?
init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.action = action
self.getController = getController
@ -72,7 +77,22 @@ final class ContextActionNode: ASDisplayNode {
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
self.iconNode.image = action.icon(presentationData.theme)
if action.iconSource == nil {
self.iconNode.image = action.icon(presentationData.theme)
}
self.badgeBackgroundNode = ASImageNode()
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.contextMenu.badgeFillColor)
self.badgeBackgroundNode.isAccessibilityElement = false
self.badgeBackgroundNode.displaysAsynchronously = false
self.badgeBackgroundNode.displayWithoutProcessing = true
self.badgeBackgroundNode.isUserInteractionEnabled = false
self.badgeTextNode = ImmediateTextNode()
self.badgeTextNode.attributedText = NSAttributedString(string: action.badge, font: Font.regular(14.0), textColor: presentationData.theme.contextMenu.badgeForegroundColor)
self.badgeTextNode.isAccessibilityElement = false
self.badgeTextNode.isUserInteractionEnabled = false
self.badgeTextNode.displaysAsynchronously = false
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.isAccessibilityElement = true
@ -85,6 +105,8 @@ final class ContextActionNode: ASDisplayNode {
self.addSubnode(self.textNode)
self.statusNode.flatMap(self.addSubnode)
self.addSubnode(self.iconNode)
self.addSubnode(self.badgeBackgroundNode)
self.addSubnode(self.badgeTextNode)
self.addSubnode(self.buttonNode)
self.buttonNode.highligthedChanged = { [weak self] highligted in
@ -99,6 +121,20 @@ final class ContextActionNode: ASDisplayNode {
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
if let iconSource = action.iconSource {
self.iconDisposable = (iconSource.signal
|> deliverOnMainQueue).start(next: { [weak self] image in
guard let strongSelf = self else {
return
}
strongSelf.iconNode.image = image
})
}
}
deinit {
self.iconDisposable?.dispose()
}
func updateLayout(constrainedWidth: CGFloat, previous: ContextActionSibling, next: ContextActionSibling) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
@ -106,7 +142,12 @@ final class ContextActionNode: ASDisplayNode {
let iconSideInset: CGFloat = 12.0
let verticalInset: CGFloat = 12.0
let iconSize = self.iconNode.image.flatMap({ $0.size }) ?? CGSize()
let iconSize: CGSize
if let iconSource = self.action.iconSource {
iconSize = iconSource.size
} else {
iconSize = self.iconNode.image.flatMap({ $0.size }) ?? CGSize()
}
let standardIconWidth: CGFloat = 32.0
var rightTextInset: CGFloat = sideInset
@ -114,17 +155,36 @@ final class ContextActionNode: ASDisplayNode {
rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset
}
let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))
let statusSize = self.statusNode?.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) ?? CGSize()
let badgeTextSize = self.badgeTextNode.updateLayout(CGSize(width: constrainedWidth, height: .greatestFiniteMagnitude))
let badgeInset: CGFloat = 4.0
let badgeSize: CGSize
let badgeWidthSpace: CGFloat
let badgeSpacing: CGFloat = 10.0
if badgeTextSize.width.isZero {
badgeSize = CGSize()
badgeWidthSpace = 0.0
} else {
badgeSize = CGSize(width: max(18.0, badgeTextSize.width + badgeInset * 2.0), height: 18.0)
badgeWidthSpace = badgeSize.width + badgeSpacing
}
let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset - badgeWidthSpace, height: .greatestFiniteMagnitude))
let statusSize = self.statusNode?.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset - badgeWidthSpace, height: .greatestFiniteMagnitude)) ?? CGSize()
if !statusSize.width.isZero, let statusNode = self.statusNode {
let verticalSpacing: CGFloat = 2.0
let combinedTextHeight = textSize.height + verticalSpacing + statusSize.height
return (CGSize(width: max(textSize.width, statusSize.width) + sideInset + rightTextInset, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize))
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
transition.updateFrameAdditive(node: statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin + verticalSpacing + textSize.height), size: textSize))
let badgeFrame = CGRect(origin: CGPoint(x: textFrame.maxX + badgeSpacing, y: floor((size.height - badgeSize.height) / 2.0)), size: badgeSize)
transition.updateFrame(node: self.badgeBackgroundNode, frame: badgeFrame)
transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: badgeFrame.minX + floorToScreenPixels((badgeFrame.width - badgeTextSize.width) / 2.0), y: badgeFrame.minY + floor((badgeFrame.height - badgeTextSize.height) / 2.0)), size: badgeTextSize))
if !iconSize.width.isZero {
transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
}
@ -136,12 +196,17 @@ final class ContextActionNode: ASDisplayNode {
} else {
return (CGSize(width: textSize.width + sideInset + rightTextInset, height: verticalInset * 2.0 + textSize.height), { size, transition in
let verticalOrigin = floor((size.height - textSize.height) / 2.0)
transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize))
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
if !iconSize.width.isZero {
transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
}
let badgeFrame = CGRect(origin: CGPoint(x: textFrame.maxX + badgeSpacing, y: floor((size.height - badgeSize.height) / 2.0)), size: badgeSize)
transition.updateFrame(node: self.badgeBackgroundNode, frame: badgeFrame)
transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: badgeFrame.minX + floorToScreenPixels((badgeFrame.width - badgeTextSize.width) / 2.0), y: badgeFrame.minY + floor((badgeFrame.height - badgeTextSize.height) / 2.0)), size: badgeTextSize))
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
@ -172,7 +237,12 @@ final class ContextActionNode: ASDisplayNode {
break
}
self.iconNode.image = self.action.icon(presentationData.theme)
if self.action.iconSource == nil {
self.iconNode.image = self.action.icon(presentationData.theme)
}
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.contextMenu.badgeFillColor)
self.badgeTextNode.attributedText = NSAttributedString(string: self.badgeTextNode.attributedText?.string ?? "", font: Font.regular(14.0), textColor: presentationData.theme.contextMenu.badgeForegroundColor)
}
@objc private func buttonPressed() {

View File

@ -28,18 +28,32 @@ public enum ContextMenuActionResult {
case custom(ContainedViewLayoutTransition)
}
public struct ContextMenuActionItemIconSource {
public let size: CGSize
public let signal: Signal<UIImage?, NoError>
public init(size: CGSize, signal: Signal<UIImage?, NoError>) {
self.size = size
self.signal = signal
}
}
public final class ContextMenuActionItem {
public let text: String
public let textColor: ContextMenuActionItemTextColor
public let textLayout: ContextMenuActionItemTextLayout
public let badge: String
public let icon: (PresentationTheme) -> UIImage?
public let iconSource: ContextMenuActionItemIconSource?
public let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void
public init(text: String, textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, icon: @escaping (PresentationTheme) -> UIImage?, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) {
public init(text: String, textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, badge: String = "", icon: @escaping (PresentationTheme) -> UIImage?, iconSource: ContextMenuActionItemIconSource? = nil, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) {
self.text = text
self.textColor = textColor
self.textLayout = textLayout
self.badge = badge
self.icon = icon
self.iconSource = iconSource
self.action = action
}
}
@ -413,7 +427,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
let takenViewInfo = source.takeView()
if let takenViewInfo = takenViewInfo, let parentSupernode = takenViewInfo.contentContainingNode.supernode {
self.contentContainerNode.contentNode = .extracted(takenViewInfo.contentContainingNode)
self.contentContainerNode.contentNode = .extracted(node: takenViewInfo.contentContainingNode, keepInPlace: source.keepInPlace)
let contentParentNode = takenViewInfo.contentContainingNode
takenViewInfo.contentContainingNode.layoutUpdated = { [weak contentParentNode, weak self] size in
guard let strongSelf = self, let contentParentNode = contentParentNode, let parentSupernode = contentParentNode.supernode else {
@ -544,7 +558,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if let contentNode = self.contentContainerNode.contentNode {
switch contentNode {
case let .extracted(extracted):
case let .extracted(extracted, keepInPlace):
let springDuration: Double = 0.42 * animationDurationFactor
let springDamping: CGFloat = 104.0
@ -564,6 +578,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
contentParentNode.applyAbsoluteOffsetSpring?(-contentContainerOffset.y, springDuration, springDamping)
}
extracted.willUpdateIsExtractedToContextPreview?(true, .animated(duration: 0.2, curve: .easeInOut))
case .controller:
let springDuration: Double = 0.52 * animationDurationFactor
let springDamping: CGFloat = 110.0
@ -620,7 +636,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
switch self.source {
case let .extracted(source):
guard let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode) = maybeContentNode else {
guard let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode, _) = maybeContentNode else {
return
}
@ -663,8 +679,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
}
contentParentNode.willUpdateIsExtractedToContextPreview?(false)
let intermediateCompletion: () -> Void = { [weak contentParentNode] in
if completedEffect && completedContentNode && completedActionsNode {
switch result {
@ -749,6 +763,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if let reactionContextNode = self.reactionContextNode {
reactionContextNode.animateOut(to: CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: contentParentNode.contentRect.size), animatingOutToReaction: self.reactionContextNodeIsAnimatingOut)
}
contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut))
} else {
if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree() {
self.contentContainerNode.view.addSubview(snapshotView)
@ -763,7 +779,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
completedContentNode = true
intermediateCompletion()
})
//self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false)
contentParentNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut))
if let reactionContextNode = self.reactionContextNode {
reactionContextNode.animateOut(to: nil, animatingOutToReaction: self.reactionContextNodeIsAnimatingOut)
@ -1054,8 +1072,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if let contentNode = self.contentContainerNode.contentNode {
switch contentNode {
case let .extracted(contentParentNode):
let contentActionsSpacing: CGFloat = 8.0
case let .extracted(contentParentNode, keepInPlace):
let contentActionsSpacing: CGFloat = keepInPlace ? 16.0 : 8.0
if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame {
let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
@ -1065,8 +1083,23 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition)
let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height)
var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - actionsSize.width - actionsSideInset, originalProjectedContentViewFrame.1.minX)), y: min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin)), size: actionsSize)
var originalContentFrame = CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalActionsFrame.minY - contentActionsSpacing - originalProjectedContentViewFrame.1.size.height), size: originalProjectedContentViewFrame.1.size)
let preferredActionsX: CGFloat
let originalActionsY: CGFloat
if keepInPlace {
originalActionsY = originalProjectedContentViewFrame.1.minY - contentActionsSpacing - actionsSize.height
preferredActionsX = originalProjectedContentViewFrame.1.maxX - actionsSize.width
} else {
originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin)
preferredActionsX = originalProjectedContentViewFrame.1.minX
}
var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - actionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: actionsSize)
let originalContentY: CGFloat
if keepInPlace {
originalContentY = originalProjectedContentViewFrame.1.minY
} else {
originalContentY = originalActionsFrame.minY - contentActionsSpacing - originalProjectedContentViewFrame.1.size.height
}
var originalContentFrame = CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalContentY), size: originalProjectedContentViewFrame.1.size)
let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0)
if originalContentFrame.minY < topEdge {
let requiredOffset = topEdge - originalContentFrame.minY
@ -1262,7 +1295,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
guard let layout = self.validLayout else {
return
}
if let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode) = maybeContentNode {
if let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode, _) = maybeContentNode {
let contentContainerFrame = self.contentContainerNode.frame
contentParentNode.updateAbsoluteRect?(contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y), layout.size)
}
@ -1280,7 +1313,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
let mappedPoint = self.view.convert(point, to: self.scrollNode.view)
if let maybeContentNode = self.contentContainerNode.contentNode {
switch maybeContentNode {
case let .extracted(contentParentNode):
case let .extracted(contentParentNode, _):
let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view)
if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) {
if result is TextSelectionNodeView {
@ -1346,6 +1379,8 @@ public final class ContextControllerPutBackViewInfo {
}
public protocol ContextExtractedContentSource: class {
var keepInPlace: Bool { get }
func takeView() -> ContextControllerTakeViewInfo?
func putBack() -> ContextControllerPutBackViewInfo?
}

View File

@ -328,12 +328,14 @@ public extension ContainedViewLayoutTransition {
}
}
func animateHorizontalOffsetAdditive(node: ASDisplayNode, offset: CGFloat) {
func animateHorizontalOffsetAdditive(node: ASDisplayNode, offset: CGFloat, completion: (() -> Void)? = nil) {
switch self {
case .immediate:
break
case let .animated(duration, curve):
node.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction)
node.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { _ in
completion?()
})
}
}

View File

@ -2,14 +2,14 @@ import Foundation
import AsyncDisplayKit
import Display
final class ContextContentContainerNode: ASDisplayNode {
var contentNode: ContextContentNode?
public final class ContextContentContainerNode: ASDisplayNode {
public var contentNode: ContextContentNode?
override init() {
override public init() {
super.init()
}
func updateLayout(size: CGSize, scaledSize: CGSize, transition: ContainedViewLayoutTransition) {
public func updateLayout(size: CGSize, scaledSize: CGSize, transition: ContainedViewLayoutTransition) {
guard let contentNode = self.contentNode else {
return
}

View File

@ -6,7 +6,7 @@ public final class ContextExtractedContentContainingNode: ASDisplayNode {
public let contentNode: ContextExtractedContentNode
public var contentRect: CGRect = CGRect()
public var isExtractedToContextPreview: Bool = false
public var willUpdateIsExtractedToContextPreview: ((Bool) -> Void)?
public var willUpdateIsExtractedToContextPreview: ((Bool, ContainedViewLayoutTransition) -> Void)?
public var isExtractedToContextPreviewUpdated: ((Bool) -> Void)?
public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)?
public var applyAbsoluteOffset: ((CGFloat, ContainedViewLayoutTransitionCurve, Double) -> Void)?
@ -26,12 +26,12 @@ public final class ContextExtractedContentContainingNode: ASDisplayNode {
public final class ContextExtractedContentNode: ASDisplayNode {
}
final class ContextControllerContentNode: ASDisplayNode {
let sourceNode: ASDisplayNode
let controller: ViewController
public final class ContextControllerContentNode: ASDisplayNode {
public let sourceNode: ASDisplayNode
public let controller: ViewController
private let tapped: () -> Void
init(sourceNode: ASDisplayNode, controller: ViewController, tapped: @escaping () -> Void) {
public init(sourceNode: ASDisplayNode, controller: ViewController, tapped: @escaping () -> Void) {
self.sourceNode = sourceNode
self.controller = controller
self.tapped = tapped
@ -41,7 +41,7 @@ final class ContextControllerContentNode: ASDisplayNode {
self.addSubnode(controller.displayNode)
}
override func didLoad() {
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
@ -53,12 +53,12 @@ final class ContextControllerContentNode: ASDisplayNode {
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size))
}
}
enum ContextContentNode {
case extracted(ContextExtractedContentContainingNode)
public enum ContextContentNode {
case extracted(node: ContextExtractedContentContainingNode, keepInPlace: Bool)
case controller(ContextControllerContentNode)
}

View File

@ -13,6 +13,7 @@ public final class ContextControllerSourceNode: ASDisplayNode {
public var activated: ((ContextGesture) -> Void)?
public var shouldBegin: ((CGPoint) -> Bool)?
public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)?
public var targetNodeForActivationProgress: ASDisplayNode?
public func cancelGesture() {
self.contextGesture?.cancel()
@ -41,17 +42,19 @@ public final class ContextControllerSourceNode: ASDisplayNode {
if let customActivationProgress = strongSelf.customActivationProgress {
customActivationProgress(progress, update)
} else {
let targetNode = strongSelf.targetNodeForActivationProgress ?? strongSelf
let minScale: CGFloat = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width
let currentScale = 1.0 * (1.0 - progress) + minScale * progress
switch update {
case .update:
strongSelf.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0)
targetNode.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0)
case .begin:
strongSelf.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0)
targetNode.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0)
case let .ended(previousProgress):
let previousScale = 1.0 * (1.0 - previousProgress) + minScale * previousProgress
strongSelf.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0)
strongSelf.layer.animateSpring(from: previousScale as NSNumber, to: currentScale as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5, delay: 0.0, initialVelocity: 0.0, damping: 90.0)
targetNode.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0)
targetNode.layer.animateSpring(from: previousScale as NSNumber, to: currentScale as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5, delay: 0.0, initialVelocity: 0.0, damping: 90.0)
}
}
}

View File

@ -208,8 +208,9 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg
super.touchesCancelled(touches, with: event)
if let touch = touches.first, !self.currentProgress.isZero, self.isValidated {
let previousProgress = self.currentProgress
self.currentProgress = 0.0
self.activationProgress?(0.0, .ended(self.currentProgress))
self.activationProgress?(0.0, .ended(previousProgress))
}
self.delayTimer?.invalidate()
@ -220,8 +221,9 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg
public func cancel() {
if !self.currentProgress.isZero, self.isValidated {
let previousProgress = self.currentProgress
self.currentProgress = 0.0
self.activationProgress?(0.0, .ended(self.currentProgress))
self.activationProgress?(0.0, .ended(previousProgress))
self.delayTimer?.invalidate()
self.animator?.invalidate()

View File

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

View File

@ -14,8 +14,10 @@ public final class TabBarControllerTheme {
public let tabBarBadgeBackgroundColor: UIColor
public let tabBarBadgeStrokeColor: UIColor
public let tabBarBadgeTextColor: UIColor
public let tabBarExtractedIconColor: UIColor
public let tabBarExtractedTextColor: UIColor
public init(backgroundColor: UIColor, tabBarBackgroundColor: UIColor, tabBarSeparatorColor: UIColor, tabBarIconColor: UIColor, tabBarSelectedIconColor: UIColor, tabBarTextColor: UIColor, tabBarSelectedTextColor: UIColor, tabBarBadgeBackgroundColor: UIColor, tabBarBadgeStrokeColor: UIColor, tabBarBadgeTextColor: UIColor) {
public init(backgroundColor: UIColor, tabBarBackgroundColor: UIColor, tabBarSeparatorColor: UIColor, tabBarIconColor: UIColor, tabBarSelectedIconColor: UIColor, tabBarTextColor: UIColor, tabBarSelectedTextColor: UIColor, tabBarBadgeBackgroundColor: UIColor, tabBarBadgeStrokeColor: UIColor, tabBarBadgeTextColor: UIColor, tabBarExtractedIconColor: UIColor, tabBarExtractedTextColor: UIColor) {
self.backgroundColor = backgroundColor
self.tabBarBackgroundColor = tabBarBackgroundColor
self.tabBarSeparatorColor = tabBarSeparatorColor
@ -26,6 +28,8 @@ public final class TabBarControllerTheme {
self.tabBarBadgeBackgroundColor = tabBarBadgeBackgroundColor
self.tabBarBadgeStrokeColor = tabBarBadgeStrokeColor
self.tabBarBadgeTextColor = tabBarBadgeTextColor
self.tabBarExtractedIconColor = tabBarExtractedIconColor
self.tabBarExtractedTextColor = tabBarExtractedTextColor
}
}
@ -229,6 +233,13 @@ open class TabBarController: ViewController {
}
}))
}
}, contextAction: { [weak self] index, node, gesture in
guard let strongSelf = self else {
return
}
if index >= 0 && index < strongSelf.controllers.count {
strongSelf.controllers[index].tabBarItemContextAction(sourceNode: node, gesture: gesture)
}
}, toolbarActionSelected: { [weak self] action in
self?.currentController?.toolbarActionSelected(action: action)
})
@ -367,7 +378,7 @@ open class TabBarController: ViewController {
}
}
self.controllers = controllers
self.tabBarControllerNode.tabBarNode.tabBarItems = self.controllers.map({ $0.tabBarItem })
self.tabBarControllerNode.tabBarNode.tabBarItems = self.controllers.map({ TabBarNodeItem(item: $0.tabBarItem, hasContext: $0.hasTabBarItemContextAction) })
let signals = combineLatest(self.controllers.map({ $0.tabBarItem }).map { tabBarItem -> Signal<Bool, NoError> in
if let tabBarItem = tabBarItem, tabBarItem.image == nil {

View File

@ -89,26 +89,63 @@ private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor:
private let badgeFont = Font.regular(13.0)
private final class TabBarItemNode: ASDisplayNode {
let extractedContainerNode: ContextExtractedContentContainingNode
let containerNode: ContextControllerSourceNode
let imageNode: ASImageNode
let textImageNode: ASImageNode
let contextImageNode: ASImageNode
let contextTextImageNode: ASImageNode
var contentWidth: CGFloat?
override init() {
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.imageNode = ASImageNode()
self.imageNode.isUserInteractionEnabled = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.displaysAsynchronously = false
self.imageNode.isAccessibilityElement = false
self.textImageNode = ASImageNode()
self.textImageNode.isUserInteractionEnabled = false
self.textImageNode.displayWithoutProcessing = true
self.textImageNode.displaysAsynchronously = false
self.textImageNode.isAccessibilityElement = false
self.contextImageNode = ASImageNode()
self.contextImageNode.isUserInteractionEnabled = false
self.contextImageNode.displayWithoutProcessing = true
self.contextImageNode.displaysAsynchronously = false
self.contextImageNode.isAccessibilityElement = false
self.contextImageNode.alpha = 0.0
self.contextTextImageNode = ASImageNode()
self.contextTextImageNode.isUserInteractionEnabled = false
self.contextTextImageNode.displayWithoutProcessing = true
self.contextTextImageNode.displaysAsynchronously = false
self.contextTextImageNode.isAccessibilityElement = false
self.contextTextImageNode.alpha = 0.0
super.init()
self.isAccessibilityElement = true
self.addSubnode(self.textImageNode)
self.addSubnode(self.imageNode)
self.extractedContainerNode.contentNode.addSubnode(self.textImageNode)
self.extractedContainerNode.contentNode.addSubnode(self.imageNode)
self.extractedContainerNode.contentNode.addSubnode(self.contextTextImageNode)
self.extractedContainerNode.contentNode.addSubnode(self.contextImageNode)
self.containerNode.addSubnode(self.extractedContainerNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addSubnode(self.containerNode)
self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self else {
return
}
transition.updateAlpha(node: strongSelf.imageNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.textImageNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.contextImageNode, alpha: isExtracted ? 1.0 : 0.0)
transition.updateAlpha(node: strongSelf.contextTextImageNode, alpha: isExtracted ? 1.0 : 0.0)
}
}
}
@ -136,8 +173,8 @@ private final class TabBarNodeContainer {
var selectedImageValue: UIImage?
var appliedSelectedImageValue: UIImage?
init(item: UITabBarItem, imageNode: TabBarItemNode, updateBadge: @escaping (String) -> Void, updateTitle: @escaping (String, Bool) -> Void, updateImage: @escaping (UIImage?) -> Void, updateSelectedImage: @escaping (UIImage?) -> Void) {
self.item = item
init(item: TabBarNodeItem, imageNode: TabBarItemNode, updateBadge: @escaping (String) -> Void, updateTitle: @escaping (String, Bool) -> Void, updateImage: @escaping (UIImage?) -> Void, updateSelectedImage: @escaping (UIImage?) -> Void, contextAction: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) {
self.item = item.item
self.imageNode = imageNode
self.imageNode.isAccessibilityElement = true
@ -162,25 +199,33 @@ private final class TabBarNodeContainer {
self.badgeContainerNode.addSubnode(self.badgeBackgroundNode)
self.badgeContainerNode.addSubnode(self.badgeTextNode)
self.badgeValue = item.badgeValue ?? ""
self.updateBadgeListenerIndex = UITabBarItem_addSetBadgeListener(item, { value in
self.badgeValue = item.item.badgeValue ?? ""
self.updateBadgeListenerIndex = UITabBarItem_addSetBadgeListener(item.item, { value in
updateBadge(value ?? "")
})
self.titleValue = item.title
self.updateTitleListenerIndex = item.addSetTitleListener { value, animated in
self.titleValue = item.item.title
self.updateTitleListenerIndex = item.item.addSetTitleListener { value, animated in
updateTitle(value ?? "", animated)
}
self.imageValue = item.image
self.updateImageListenerIndex = item.addSetImageListener { value in
self.imageValue = item.item.image
self.updateImageListenerIndex = item.item.addSetImageListener { value in
updateImage(value)
}
self.selectedImageValue = item.selectedImage
self.updateSelectedImageListenerIndex = item.addSetSelectedImageListener { value in
self.selectedImageValue = item.item.selectedImage
self.updateSelectedImageListenerIndex = item.item.addSetSelectedImageListener { value in
updateSelectedImage(value)
}
imageNode.containerNode.activated = { [weak self] gesture in
guard let strongSelf = self else {
return
}
contextAction(strongSelf.imageNode.extractedContainerNode, gesture)
}
imageNode.containerNode.isGestureEnabled = item.hasContext
}
deinit {
@ -191,8 +236,18 @@ private final class TabBarNodeContainer {
}
}
final class TabBarNodeItem {
let item: UITabBarItem
let hasContext: Bool
init(item: UITabBarItem, hasContext: Bool) {
self.item = item
self.hasContext = hasContext
}
}
class TabBarNode: ASDisplayNode {
var tabBarItems: [UITabBarItem] = [] {
var tabBarItems: [TabBarNodeItem] = [] {
didSet {
self.reloadTabBarItems()
}
@ -213,6 +268,7 @@ class TabBarNode: ASDisplayNode {
}
private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void
private let contextAction: (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void
private var theme: TabBarControllerTheme
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)?
@ -224,8 +280,11 @@ class TabBarNode: ASDisplayNode {
let separatorNode: ASDisplayNode
private var tabBarNodeContainers: [TabBarNodeContainer] = []
init(theme: TabBarControllerTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void) {
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
init(theme: TabBarControllerTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void) {
self.itemSelected = itemSelected
self.contextAction = contextAction
self.theme = theme
self.separatorNode = ASDisplayNode()
@ -248,11 +307,25 @@ class TabBarNode: ASDisplayNode {
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(TabBarTapRecognizer(tap: { [weak self] point in
self?.tapped(at: point, longTap: false)
}, longTap: { [weak self] point in
self?.tapped(at: point, longTap: true)
}))
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .keepWithSingleTap
}
self.tapRecognizer = recognizer
self.view.addGestureRecognizer(recognizer)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
if case .tap = gesture {
self.tapped(at: location, longTap: false)
}
}
default:
break
}
}
func updateTheme(_ theme: TabBarControllerTheme) {
@ -289,7 +362,6 @@ class TabBarNode: ASDisplayNode {
private func reloadTabBarItems() {
for node in self.tabBarNodeContainers {
node.imageNode.removeFromSupernode()
node.badgeContainerNode.removeFromSupernode()
}
self.centered = self.theme.tabBarTextColor == .clear
@ -298,7 +370,6 @@ class TabBarNode: ASDisplayNode {
for i in 0 ..< self.tabBarItems.count {
let item = self.tabBarItems[i]
let node = TabBarItemNode()
node.isUserInteractionEnabled = false
let container = TabBarNodeContainer(item: item, imageNode: node, updateBadge: { [weak self] value in
self?.updateNodeBadge(i, value: value)
}, updateTitle: { [weak self] _, _ in
@ -307,31 +378,31 @@ class TabBarNode: ASDisplayNode {
self?.updateNodeImage(i, layout: true)
}, updateSelectedImage: { [weak self] _ in
self?.updateNodeImage(i, layout: true)
}, contextAction: { [weak self] node, gesture in
self?.tapRecognizer?.cancel()
self?.contextAction(i, node, gesture)
})
if let selectedIndex = self.selectedIndex, selectedIndex == i {
let (textImage, contentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
let (textImage, contentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
node.textImageNode.image = textImage
node.imageNode.image = image
node.accessibilityLabel = item.title
node.accessibilityLabel = item.item.title
node.contentWidth = max(contentWidth, imageContentWidth)
} else {
let (textImage, contentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
let (textImage, contentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
node.textImageNode.image = textImage
node.accessibilityLabel = item.title
node.accessibilityLabel = item.item.title
node.imageNode.image = image
node.contentWidth = max(contentWidth, imageContentWidth)
}
container.badgeBackgroundNode.image = self.badgeImage
node.extractedContainerNode.contentNode.addSubnode(container.badgeContainerNode)
tabBarNodeContainers.append(container)
self.addSubnode(node)
}
for container in tabBarNodeContainers {
self.addSubnode(container.badgeContainerNode)
}
self.tabBarNodeContainers = tabBarNodeContainers
self.setNeedsLayout()
@ -347,18 +418,26 @@ class TabBarNode: ASDisplayNode {
let previousImageSize = node.imageNode.image?.size ?? CGSize()
let previousTextImageSize = node.textImageNode.image?.size ?? CGSize()
if let selectedIndex = self.selectedIndex, selectedIndex == index {
let (textImage, contentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
let (textImage, contentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
let (contextTextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarExtractedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (contextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarExtractedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
node.textImageNode.image = textImage
node.accessibilityLabel = item.title
node.accessibilityLabel = item.item.title
node.imageNode.image = image
node.contextTextImageNode.image = contextTextImage
node.contextImageNode.image = contextImage
node.contentWidth = max(contentWidth, imageContentWidth)
} else {
let (textImage, contentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
let (textImage, contentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (image, imageContentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
let (contextTextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarExtractedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered)
let (contextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarExtractedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered)
node.textImageNode.image = textImage
node.accessibilityLabel = item.title
node.accessibilityLabel = item.item.title
node.imageNode.image = image
node.contextTextImageNode.image = contextTextImage
node.contextImageNode.image = contextImage
node.contentWidth = max(contentWidth, imageContentWidth)
}
@ -418,8 +497,14 @@ class TabBarNode: ASDisplayNode {
let originX = floor(leftNodeOriginX + CGFloat(i) * distanceBetweenNodes - nodeSize.width / 2.0)
let nodeFrame = CGRect(origin: CGPoint(x: originX, y: 3.0), size: nodeSize)
transition.updateFrame(node: node, frame: nodeFrame)
node.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
node.extractedContainerNode.contentNode.frame = node.extractedContainerNode.bounds
node.extractedContainerNode.contentRect = node.extractedContainerNode.bounds
node.containerNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
node.imageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
node.textImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
node.contextImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
node.contextTextImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size)
if container.badgeValue != container.appliedBadgeValue {
container.appliedBadgeValue = container.badgeValue
@ -440,10 +525,10 @@ class TabBarNode: ASDisplayNode {
let backgroundSize = CGSize(width: hasSingleLetterValue ? 18.0 : max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0)
let backgroundFrame: CGRect
if horizontal {
backgroundFrame = CGRect(origin: CGPoint(x: originX + 15.0, y: 3.0), size: backgroundSize)
backgroundFrame = CGRect(origin: CGPoint(x: 15.0, y: 0.0), size: backgroundSize)
} else {
let contentWidth: CGFloat = 25.0 //node.contentWidth ?? node.frame.width
backgroundFrame = CGRect(origin: CGPoint(x: floor(originX + node.frame.width / 2.0) + contentWidth - backgroundSize.width - 5.0, y: self.centered ? 9.0 : 2.0), size: backgroundSize)
let contentWidth: CGFloat = 25.0
backgroundFrame = CGRect(origin: CGPoint(x: floor(node.frame.width / 2.0) + contentWidth - backgroundSize.width - 5.0, y: self.centered ? 6.0 : -1.0), size: backgroundSize)
}
transition.updateFrame(node: container.badgeContainerNode, frame: backgroundFrame)
container.badgeBackgroundNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)

View File

@ -630,6 +630,11 @@ public enum ViewControllerNavigationPresentation {
open func toolbarActionSelected(action: ToolbarActionOption) {
}
open var hasTabBarItemContextAction: Bool = false
open func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
}
}
func traceIsOpaque(layer: CALayer, rect: CGRect) -> Bool {

View File

@ -146,20 +146,6 @@
}
[self.view addSubview:_effectView];
/*
let contextMenu = PresentationThemeContextMenu(
dimColor: UIColor(rgb: 0x000a26, alpha: 0.2),
backgroundColor: UIColor(rgb: 0xf9f9f9, alpha: 0.78),
itemSeparatorColor: UIColor(rgb: 0x3c3c43, alpha: 0.2),
sectionSeparatorColor: UIColor(rgb: 0x8a8a8a, alpha: 0.2),
itemBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.0),
itemHighlightedBackgroundColor: UIColor(rgb: 0x3c3c43, alpha: 0.2),
primaryColor: UIColor(rgb: 0x000000, alpha: 1.0),
secondaryColor: UIColor(rgb: 0x000000, alpha: 0.8),
destructiveColor: UIColor(rgb: 0xff3b30)
)
*/
_containerView = [[UIView alloc] init];
if (_isDark) {
_containerView.backgroundColor = UIColorRGB(0x1f1f1f);

View File

@ -245,12 +245,31 @@ final class ChatListTable: Table {
}
}
func getUnreadChatListPeerIds(postbox: Postbox, groupId: PeerGroupId) -> [PeerId] {
func getUnreadChatListPeerIds(postbox: Postbox, groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?) -> [PeerId] {
var result: [PeerId] = []
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), keys: { key in
let (_, _, messageIndex, _) = extractKey(key)
if let state = postbox.readStateTable.getCombinedState(messageIndex.id.peerId), state.isUnread {
result.append(messageIndex.id.peerId)
let passFilter: Bool
if let filterPredicate = filterPredicate {
if let peer = postbox.peerTable.get(messageIndex.id.peerId) {
let isUnread = postbox.readStateTable.getCombinedState(messageIndex.id.peerId)?.isUnread ?? false
if filterPredicate(peer, postbox.peerNotificationSettingsTable.getEffective(messageIndex.id.peerId), isUnread) {
passFilter = true
} else {
passFilter = false
}
} else {
passFilter = false
}
} else {
passFilter = true
}
if passFilter {
result.append(messageIndex.id.peerId)
}
}
return true
}, limit: 0)

View File

@ -318,7 +318,7 @@ final class MutableChatListView {
self.count = count
self.summaryComponents = summaryComponents
self.additionalItemEntries = []
if case .root = groupId {
if case .root = groupId, self.filterPredicate == nil {
let itemIds = postbox.additionalChatListItemsTable.get()
self.additionalItemIds = Set(itemIds)
for peerId in itemIds {
@ -336,7 +336,7 @@ final class MutableChatListView {
private func reloadGroups(postbox: Postbox) {
self.groupEntries.removeAll()
if case .root = self.groupId {
if case .root = self.groupId, self.filterPredicate == nil {
for groupId in postbox.chatListTable.existingGroups() {
var foundIndices: [(ChatListIndex, MessageIndex)] = []
var unpinnedCount = 0
@ -456,7 +456,7 @@ final class MutableChatListView {
}
}
if case .root = self.groupId {
if case .root = self.groupId, self.filterPredicate == nil {
var invalidatedGroups = false
for (groupId, groupOperations) in operations {
if case .group = groupId, !groupOperations.isEmpty {

View File

@ -303,10 +303,10 @@ public final class Transaction {
return self.postbox?.chatListTable.getPeerChatListIndex(peerId: peerId)
}
public func getUnreadChatListPeerIds(groupId: PeerGroupId) -> [PeerId] {
public func getUnreadChatListPeerIds(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?) -> [PeerId] {
assert(!self.disposed)
if let postbox = self.postbox {
return postbox.chatListTable.getUnreadChatListPeerIds(postbox: postbox, groupId: groupId)
return postbox.chatListTable.getUnreadChatListPeerIds(postbox: postbox, groupId: groupId, filterPredicate: filterPredicate)
} else {
return []
}
@ -1053,7 +1053,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration,
}
public final class Postbox {
private let queue: Queue
public let queue: Queue
public let seedConfiguration: SeedConfiguration
private let basePath: String
let valueBox: SqliteValueBox

View File

@ -722,7 +722,7 @@ public protocol SettingsController: class {
func updateContext(context: AccountContext)
}
private final class SettingsControllerImpl: ItemListController, SettingsController, TabBarContainedController {
private final class SettingsControllerImpl: ItemListController, SettingsController {
let sharedContext: SharedAccountContext
let contextValue: Promise<AccountContext>
var accountsAndPeersValue: ((Account, Peer)?, [(Account, Peer, Int32)])?
@ -731,8 +731,6 @@ private final class SettingsControllerImpl: ItemListController, SettingsControll
var switchToAccount: ((AccountRecordId) -> Void)?
var addAccount: (() -> Void)?
weak var switchController: TabBarAccountSwitchController?
override var navigationBarRequiresEntireLayoutUpdate: Bool {
return false
}
@ -751,6 +749,8 @@ private final class SettingsControllerImpl: ItemListController, SettingsControll
super.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: updatedPresentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: tabBarItem)
self.hasTabBarItemContextAction = true
self.accountsAndPeersDisposable = (accountsAndPeers
|> deliverOnMainQueue).start(next: { [weak self] value in
self?.accountsAndPeersValue = value
@ -769,7 +769,7 @@ private final class SettingsControllerImpl: ItemListController, SettingsControll
//self.contextValue.set(.single(context))
}
func presentTabBarPreviewingController(sourceNodes: [ASDisplayNode]) {
/*func presentTabBarPreviewingController(sourceNodes: [ASDisplayNode]) {
guard let (maybePrimary, other) = self.accountsAndPeersValue, let primary = maybePrimary else {
return
}
@ -778,11 +778,98 @@ private final class SettingsControllerImpl: ItemListController, SettingsControll
}, addAccount: { [weak self] in
self?.addAccount?()
}, sourceNodes: sourceNodes)
self.switchController = controller
self.sharedContext.mainWindow?.present(controller, on: .root)
}
func updateTabBarPreviewingControllerPresentation(_ update: TabBarContainedControllerPresentationUpdate) {
}*/
override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) {
guard let (maybePrimary, other) = self.accountsAndPeersValue, let primary = maybePrimary else {
return
}
let presentationData = self.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var items: [ContextMenuItem] = []
if other.count + 1 < maximumNumberOfAccounts {
items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
guard let strongSelf = self else {
return
}
strongSelf.addAccount?()
f(.dismissWithoutContent)
})))
}
items.append(.separator)
func accountIconSignal(account: Account, peer: Peer, size: CGSize) -> Signal<UIImage?, NoError> {
let iconSignal: Signal<UIImage?, NoError>
if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.first, displayDimensions: size, inset: 0.0, emptyColor: nil, synchronousLoad: false) {
iconSignal = signal
|> map { imageVersions -> UIImage? in
return imageVersions?.0
}
} else {
let peerId = peer.id
let displayLetters = peer.displayLetters
iconSignal = Signal { subscriber in
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
drawPeerAvatarLetters(context: context, size: CGSize(width: size.width, height: size.height), font: avatarFont, letters: displayLetters, peerId: peerId)
})?.withRenderingMode(.alwaysOriginal)
subscriber.putNext(image)
subscriber.putCompletion()
return EmptyDisposable
}
}
return iconSignal
}
let avatarSize = CGSize(width: 28.0, height: 28.0)
items.append(.action(ContextMenuActionItem(text: primary.1.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: accountIconSignal(account: primary.0, peer: primary.1, size: avatarSize)), action: { _, f in
f(.default)
})))
for account in other {
let id = account.0.id
items.append(.action(ContextMenuActionItem(text: account.1.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), badge: account.2 != 0 ? "\(account.2)" : "", icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: accountIconSignal(account: account.0, peer: account.1, size: avatarSize)), action: { [weak self] _, f in
guard let strongSelf = self else {
return
}
strongSelf.switchToAccount?(id)
f(.dismissWithoutContent)
})))
}
let controller = ContextController(account: primary.0, presentationData: presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture)
self.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
}
}
private final class SettingsTabBarContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = true
private let controller: ViewController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
@ -1798,10 +1885,10 @@ private func accountContextMenuItems(context: AccountContext, logout: @escaping
return context.account.postbox.transaction { transaction -> [ContextMenuItem] in
var items: [ContextMenuItem] = []
if !transaction.getUnreadChatListPeerIds(groupId: .root).isEmpty {
if !transaction.getUnreadChatListPeerIds(groupId: .root, filterPredicate: nil).isEmpty {
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAllAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in
let _ = (context.account.postbox.transaction { transaction in
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: .root)
markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: .root, filterPredicate: nil)
}
|> deliverOnMainQueue).start(completed: {
f(.default)

View File

@ -161,8 +161,8 @@ public func clearPeerUnseenPersonalMessagesInteractively(account: Account, peerI
|> ignoreValues
}
public func markAllChatsAsReadInteractively(transaction: Transaction, viewTracker: AccountViewTracker, groupId: PeerGroupId) {
for peerId in transaction.getUnreadChatListPeerIds(groupId: groupId) {
public func markAllChatsAsReadInteractively(transaction: Transaction, viewTracker: AccountViewTracker, groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?) {
for peerId in transaction.getUnreadChatListPeerIds(groupId: groupId, filterPredicate: filterPredicate) {
togglePeerUnreadMarkInteractively(transaction: transaction, viewTracker: viewTracker, peerId: peerId, setToValue: false)
}
}

View File

@ -40,7 +40,7 @@ public extension PresentationFontSize {
public extension TabBarControllerTheme {
convenience init(rootControllerTheme: PresentationTheme) {
let theme = rootControllerTheme.rootController.tabBar
self.init(backgroundColor: rootControllerTheme.list.plainBackgroundColor, tabBarBackgroundColor: theme.backgroundColor, tabBarSeparatorColor: theme.separatorColor, tabBarIconColor: theme.iconColor, tabBarSelectedIconColor: theme.selectedIconColor, tabBarTextColor: theme.textColor, tabBarSelectedTextColor: theme.selectedTextColor, tabBarBadgeBackgroundColor: theme.badgeBackgroundColor, tabBarBadgeStrokeColor: theme.badgeStrokeColor, tabBarBadgeTextColor: theme.badgeTextColor)
self.init(backgroundColor: rootControllerTheme.list.plainBackgroundColor, tabBarBackgroundColor: theme.backgroundColor, tabBarSeparatorColor: theme.separatorColor, tabBarIconColor: theme.iconColor, tabBarSelectedIconColor: theme.selectedIconColor, tabBarTextColor: theme.textColor, tabBarSelectedTextColor: theme.selectedTextColor, tabBarBadgeBackgroundColor: theme.badgeBackgroundColor, tabBarBadgeStrokeColor: theme.badgeStrokeColor, tabBarBadgeTextColor: theme.badgeTextColor, tabBarExtractedIconColor: rootControllerTheme.contextMenu.extractedContentTintColor, tabBarExtractedTextColor: rootControllerTheme.contextMenu.extractedContentTintColor)
}
}

View File

@ -520,7 +520,10 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
itemHighlightedBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.15),
primaryColor: UIColor(rgb: 0xffffff, alpha: 1.0),
secondaryColor: UIColor(rgb: 0xffffff, alpha: 0.8),
destructiveColor: UIColor(rgb: 0xeb5545)
destructiveColor: UIColor(rgb: 0xeb5545),
badgeFillColor: UIColor(rgb: 0xeb5545),
badgeForegroundColor: UIColor(rgb: 0xffffff, alpha: 1.0),
extractedContentTintColor: UIColor(rgb: 0x252525, alpha: 0.78)
)
let inAppNotification = PresentationThemeInAppNotification(

View File

@ -769,7 +769,10 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres
itemHighlightedBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.15),
primaryColor: UIColor(rgb: 0xffffff, alpha: 1.0),
secondaryColor: UIColor(rgb: 0xffffff, alpha: 0.8),
destructiveColor: UIColor(rgb: 0xff6767)
destructiveColor: UIColor(rgb: 0xff6767),
badgeFillColor: UIColor(rgb: 0xff6767),
badgeForegroundColor: UIColor(rgb: 0xffffff, alpha: 1.0),
extractedContentTintColor: rootNavigationBar.backgroundColor.withAlphaComponent(0.78)
)
let inAppNotification = PresentationThemeInAppNotification(

View File

@ -731,7 +731,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
itemHighlightedBackgroundColor: UIColor(rgb: 0x3c3c43, alpha: 0.2),
primaryColor: UIColor(rgb: 0x000000, alpha: 1.0),
secondaryColor: UIColor(rgb: 0x000000, alpha: 0.8),
destructiveColor: UIColor(rgb: 0xff3b30)
destructiveColor: UIColor(rgb: 0xff3b30),
badgeFillColor: UIColor(rgb: 0xff3b30),
badgeForegroundColor: UIColor(rgb: 0xffffff, alpha: 1.0),
extractedContentTintColor: .white
)
let inAppNotification = PresentationThemeInAppNotification(

View File

@ -261,8 +261,11 @@ public final class PresentationThemeContextMenu {
public let primaryColor: UIColor
public let secondaryColor: UIColor
public let destructiveColor: UIColor
public let badgeFillColor: UIColor
public let badgeForegroundColor: UIColor
public let extractedContentTintColor: UIColor
init(dimColor: UIColor, backgroundColor: UIColor, itemSeparatorColor: UIColor, sectionSeparatorColor: UIColor, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, destructiveColor: UIColor) {
init(dimColor: UIColor, backgroundColor: UIColor, itemSeparatorColor: UIColor, sectionSeparatorColor: UIColor, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, destructiveColor: UIColor, badgeFillColor: UIColor, badgeForegroundColor: UIColor, extractedContentTintColor: UIColor) {
self.dimColor = dimColor
self.backgroundColor = backgroundColor
self.itemSeparatorColor = itemSeparatorColor
@ -272,10 +275,13 @@ public final class PresentationThemeContextMenu {
self.primaryColor = primaryColor
self.secondaryColor = secondaryColor
self.destructiveColor = destructiveColor
self.badgeFillColor = badgeFillColor
self.badgeForegroundColor = badgeForegroundColor
self.extractedContentTintColor = extractedContentTintColor
}
public func withUpdated(dimColor: UIColor? = nil, backgroundColor: UIColor? = nil, itemSeparatorColor: UIColor? = nil, sectionSeparatorColor: UIColor? = nil, itemBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, primaryColor: UIColor? = nil, secondaryColor: UIColor? = nil, destructiveColor: UIColor? = nil) -> PresentationThemeContextMenu {
return PresentationThemeContextMenu(dimColor: dimColor ?? self.dimColor, backgroundColor: backgroundColor ?? self.backgroundColor, itemSeparatorColor: itemSeparatorColor ?? self.itemSeparatorColor, sectionSeparatorColor: sectionSeparatorColor ?? self.sectionSeparatorColor, itemBackgroundColor: itemBackgroundColor ?? self.itemBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, primaryColor: primaryColor ?? self.primaryColor, secondaryColor: secondaryColor ?? self.secondaryColor, destructiveColor: destructiveColor ?? self.destructiveColor)
return PresentationThemeContextMenu(dimColor: dimColor ?? self.dimColor, backgroundColor: backgroundColor ?? self.backgroundColor, itemSeparatorColor: itemSeparatorColor ?? self.itemSeparatorColor, sectionSeparatorColor: sectionSeparatorColor ?? self.sectionSeparatorColor, itemBackgroundColor: itemBackgroundColor ?? self.itemBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, primaryColor: primaryColor ?? self.primaryColor, secondaryColor: secondaryColor ?? self.secondaryColor, destructiveColor: destructiveColor ?? self.destructiveColor, badgeFillColor: self.badgeFillColor, badgeForegroundColor: self.badgeForegroundColor, extractedContentTintColor: self.extractedContentTintColor)
}
}

View File

@ -1629,19 +1629,28 @@ extension PresentationThemeContextMenu: Codable {
case primary
case secondary
case destructive
case badgeFill
case badgeForeground
case extractedTint
}
public convenience init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.init(dimColor: try decodeColor(values, .dim),
backgroundColor: try decodeColor(values, .background),
itemSeparatorColor: try decodeColor(values, .itemSeparator),
sectionSeparatorColor: try decodeColor(values, .sectionSeparator),
itemBackgroundColor: try decodeColor(values, .itemBg),
itemHighlightedBackgroundColor: try decodeColor(values, .itemHighlightedBg),
primaryColor: try decodeColor(values, .primary),
secondaryColor: try decodeColor(values, .secondary),
destructiveColor: try decodeColor(values, .destructive)
let destructiveColor = try decodeColor(values, .destructive)
let backgroundColor = try decodeColor(values, .background)
self.init(
dimColor: try decodeColor(values, .dim),
backgroundColor: backgroundColor,
itemSeparatorColor: try decodeColor(values, .itemSeparator),
sectionSeparatorColor: try decodeColor(values, .sectionSeparator),
itemBackgroundColor: try decodeColor(values, .itemBg),
itemHighlightedBackgroundColor: try decodeColor(values, .itemHighlightedBg),
primaryColor: try decodeColor(values, .primary),
secondaryColor: try decodeColor(values, .secondary),
destructiveColor: destructiveColor,
badgeFillColor: (try? decodeColor(values, .badgeFill)) ?? destructiveColor,
badgeForegroundColor: (try? decodeColor(values, .badgeForeground)) ?? backgroundColor,
extractedContentTintColor: (try? decodeColor(values, .extractedTint)) ?? backgroundColor
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -228,7 +228,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
self?.accessibilityElementDidBecomeFocused()
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtractedToContextPreview in
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtractedToContextPreview, _ in
guard let strongSelf = self, let item = strongSelf.item else {
return
}

View File

@ -5,6 +5,8 @@ import ContextUI
import Postbox
final class ChatMessageContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = false
private weak var chatNode: ChatControllerNode?
private let message: Message

View File

@ -6,7 +6,7 @@ import SyncCore
public enum TotalUnreadCountDisplayStyle: Int32 {
case filtered = 0
var category: ChatListTotalUnreadStateCategory {
public var category: ChatListTotalUnreadStateCategory {
switch self {
case .filtered:
return .filtered
@ -18,7 +18,7 @@ public enum TotalUnreadCountDisplayCategory: Int32 {
case chats = 0
case messages = 1
var statsType: ChatListTotalUnreadStateStats {
public var statsType: ChatListTotalUnreadStateStats {
switch self {
case .chats:
return .chats