mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Filter improvements
This commit is contained in:
parent
868dcdf05d
commit
7db3e89a53
@ -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";
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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, _):
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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?
|
||||
}
|
||||
|
@ -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?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Bots.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Bots.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_bots.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Bots.imageset/ic_bots.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Bots.imageset/ic_bots.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Channels.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Channels.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_channels.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Channels.imageset/ic_channels.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Channels.imageset/ic_channels.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Groups.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Groups.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_group.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Groups.imageset/ic_group.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Groups.imageset/ic_group.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/List.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/List.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_list.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/List.imageset/ic_list.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/List.imageset/ic_list.pdf
vendored
Normal file
Binary file not shown.
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user