mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Experimental chat list filtering
This commit is contained in:
parent
5112310a32
commit
309a8b112b
@ -40,7 +40,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
||||
public var displayNotificationsFromAllAccounts: Bool
|
||||
|
||||
public static var defaultSettings: InAppNotificationSettings {
|
||||
return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: [.regularChatsAndPrivateGroups], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true)
|
||||
return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: [.privateChat, .secretChat, .bot, .privateGroup], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true)
|
||||
}
|
||||
|
||||
public init(playSounds: Bool, vibrate: Bool, displayPreviews: Bool, totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: PeerSummaryCounterTags, displayNameOnLockscreen: Bool, displayNotificationsFromAllAccounts: Bool) {
|
||||
@ -60,10 +60,25 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
||||
self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
||||
self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("cds", orElse: 0)) ?? .filtered
|
||||
self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 1)) ?? .messages
|
||||
if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") {
|
||||
if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags_2") {
|
||||
self.totalUnreadCountIncludeTags = PeerSummaryCounterTags(rawValue: value)
|
||||
} else if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") {
|
||||
var resultTags: PeerSummaryCounterTags = []
|
||||
for legacyTag in LegacyPeerSummaryCounterTags(rawValue: value) {
|
||||
if legacyTag == .regularChatsAndPrivateGroups {
|
||||
resultTags.insert(.privateChat)
|
||||
resultTags.insert(.secretChat)
|
||||
resultTags.insert(.bot)
|
||||
resultTags.insert(.privateGroup)
|
||||
} else if legacyTag == .publicGroups {
|
||||
resultTags.insert(.publicGroup)
|
||||
} else if legacyTag == .channels {
|
||||
resultTags.insert(.channel)
|
||||
}
|
||||
}
|
||||
self.totalUnreadCountIncludeTags = resultTags
|
||||
} else {
|
||||
self.totalUnreadCountIncludeTags = [.regularChatsAndPrivateGroups]
|
||||
self.totalUnreadCountIncludeTags = [.privateChat, .secretChat, .bot, .privateGroup]
|
||||
}
|
||||
self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0
|
||||
self.displayNotificationsFromAllAccounts = decoder.decodeInt32ForKey("displayNotificationsFromAllAccounts", orElse: 1) != 0
|
||||
@ -75,7 +90,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
||||
encoder.encodeInt32(self.displayPreviews ? 1 : 0, forKey: "p")
|
||||
encoder.encodeInt32(self.totalUnreadCountDisplayStyle.rawValue, forKey: "cds")
|
||||
encoder.encodeInt32(self.totalUnreadCountDisplayCategory.rawValue, forKey: "totalUnreadCountDisplayCategory")
|
||||
encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags")
|
||||
encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags_2")
|
||||
encoder.encodeInt32(self.displayNameOnLockscreen ? 1 : 0, forKey: "displayNameOnLockscreen")
|
||||
encoder.encodeInt32(self.displayNotificationsFromAllAccounts ? 1 : 0, forKey: "displayNotificationsFromAllAccounts")
|
||||
}
|
||||
|
@ -1,9 +1,43 @@
|
||||
import PostboxDataTypes
|
||||
|
||||
struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable {
|
||||
var rawValue: Int32
|
||||
|
||||
init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let regularChatsAndPrivateGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 0)
|
||||
static let publicGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 1)
|
||||
static let channels = LegacyPeerSummaryCounterTags(rawValue: 1 << 2)
|
||||
|
||||
public func makeIterator() -> AnyIterator<LegacyPeerSummaryCounterTags> {
|
||||
var index = 0
|
||||
return AnyIterator { () -> LegacyPeerSummaryCounterTags? in
|
||||
while index < 31 {
|
||||
let currentTags = self.rawValue >> UInt32(index)
|
||||
let tag = LegacyPeerSummaryCounterTags(rawValue: 1 << UInt32(index))
|
||||
index += 1
|
||||
if currentTags == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if (currentTags & 1) != 0 {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PeerSummaryCounterTags {
|
||||
static let regularChatsAndPrivateGroups = PeerSummaryCounterTags(rawValue: 1 << 0)
|
||||
static let publicGroups = PeerSummaryCounterTags(rawValue: 1 << 1)
|
||||
static let channels = PeerSummaryCounterTags(rawValue: 1 << 2)
|
||||
static let privateChat = PeerSummaryCounterTags(rawValue: 1 << 3)
|
||||
static let secretChat = PeerSummaryCounterTags(rawValue: 1 << 4)
|
||||
static let privateGroup = PeerSummaryCounterTags(rawValue: 1 << 5)
|
||||
static let bot = PeerSummaryCounterTags(rawValue: 1 << 6)
|
||||
static let channel = PeerSummaryCounterTags(rawValue: 1 << 7)
|
||||
static let publicGroup = PeerSummaryCounterTags(rawValue: 1 << 8)
|
||||
}
|
||||
|
||||
struct Namespaces {
|
||||
@ -17,4 +51,4 @@ struct Namespaces {
|
||||
static let CloudChannel: Int32 = 2
|
||||
static let SecretChat: Int32 = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,19 +94,21 @@ enum SyncProviderImpl {
|
||||
if let channel = peerTable.get(peerId) as? TelegramChannel {
|
||||
switch channel.info {
|
||||
case .broadcast:
|
||||
tag = .channels
|
||||
tag = .channel
|
||||
case .group:
|
||||
if channel.username != nil {
|
||||
tag = .publicGroups
|
||||
tag = .publicGroup
|
||||
} else {
|
||||
tag = .regularChatsAndPrivateGroups
|
||||
tag = .privateGroup
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tag = .channels
|
||||
tag = .channel
|
||||
}
|
||||
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||
tag = .privateGroup
|
||||
} else {
|
||||
tag = .regularChatsAndPrivateGroups
|
||||
tag = .privateChat
|
||||
}
|
||||
|
||||
var totalCount: Int32 = -1
|
||||
|
@ -261,11 +261,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
||||
}
|
||||
|
||||
if !self.hideNetworkActivityStatus {
|
||||
self.titleDisposable = combineLatest(queue: .mainQueue(), context.account.networkState, hasProxy, passcode, self.chatListDisplayNode.chatListNode.state).start(next: { [weak self] networkState, proxy, passcode, state in
|
||||
self.titleDisposable = combineLatest(queue: .mainQueue(),
|
||||
context.account.networkState,
|
||||
hasProxy,
|
||||
passcode,
|
||||
self.chatListDisplayNode.chatListNode.state,
|
||||
self.chatListDisplayNode.chatListNode.chatListFilterSignal
|
||||
).start(next: { [weak self] networkState, proxy, passcode, state, chatListFilter in
|
||||
if let strongSelf = self {
|
||||
let defaultTitle: String
|
||||
if case .root = strongSelf.groupId {
|
||||
defaultTitle = strongSelf.presentationData.strings.DialogList_Title
|
||||
if let chatListFilter = chatListFilter {
|
||||
let title: String
|
||||
switch chatListFilter.name {
|
||||
case .unread:
|
||||
title = "Unread"
|
||||
case let .custom(value):
|
||||
title = value
|
||||
}
|
||||
defaultTitle = title
|
||||
} else {
|
||||
defaultTitle = strongSelf.presentationData.strings.DialogList_Title
|
||||
}
|
||||
} else {
|
||||
defaultTitle = strongSelf.presentationData.strings.ChatList_ArchivedChatsTitle
|
||||
}
|
||||
@ -1793,13 +1810,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
||||
|
||||
public func presentTabBarPreviewingController(sourceNodes: [ASDisplayNode]) {
|
||||
if self.isNodeLoaded {
|
||||
let controller = TabBarChatListFilterController(context: self.context, sourceNodes: sourceNodes, currentFilter: self.chatListDisplayNode.chatListNode.chatListFilter, updateFilter: { [weak self] value in
|
||||
let _ = (self.context.account.postbox.transaction { transaction -> [ChatListFilterPreset] in
|
||||
let settings = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.chatListFilterSettings) as? ChatListFilterSettings ?? ChatListFilterSettings.default
|
||||
return settings.presets
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presetList in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.chatListDisplayNode.chatListNode.chatListFilter = value
|
||||
let controller = TabBarChatListFilterController(context: strongSelf.context, sourceNodes: sourceNodes, presetList: presetList, currentPreset: strongSelf.chatListDisplayNode.chatListNode.chatListFilter, setup: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.push(chatListFilterPresetListController(context: strongSelf.context))
|
||||
}, updatePreset: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.chatListDisplayNode.chatListNode.chatListFilter = value
|
||||
})
|
||||
strongSelf.context.sharedContext.mainWindow?.present(controller, on: .root)
|
||||
})
|
||||
self.context.sharedContext.mainWindow?.present(controller, on: .root)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,387 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import TelegramUIPreferences
|
||||
import ItemListPeerItem
|
||||
import ItemListPeerActionItem
|
||||
|
||||
private final class ChatListFilterPresetControllerArguments {
|
||||
let context: AccountContext
|
||||
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
|
||||
let openAddPeer: () -> Void
|
||||
let deleteAdditionalPeer: (PeerId) -> Void
|
||||
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
||||
|
||||
init(context: AccountContext, updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void, openAddPeer: @escaping () -> Void, deleteAdditionalPeer: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) {
|
||||
self.context = context
|
||||
self.updateState = updateState
|
||||
self.openAddPeer = openAddPeer
|
||||
self.deleteAdditionalPeer = deleteAdditionalPeer
|
||||
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChatListFilterPresetControllerSection: Int32 {
|
||||
case name
|
||||
case categories
|
||||
case excludeCategories
|
||||
case additionalPeers
|
||||
}
|
||||
|
||||
private func filterEntry(presentationData: ItemListPresentationData, arguments: ChatListFilterPresetControllerArguments, title: String, value: Bool, filter: ChatListIncludeCategoryFilter, section: Int32) -> ItemListCheckboxItem {
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: section, action: {
|
||||
arguments.updateState { current in
|
||||
var state = current
|
||||
if state.includeCategories.contains(filter) {
|
||||
state.includeCategories.remove(filter)
|
||||
} else {
|
||||
state.includeCategories.insert(filter)
|
||||
}
|
||||
return state
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private enum ChatListFilterPresetEntryStableId: Hashable {
|
||||
case index(Int)
|
||||
case peer(PeerId)
|
||||
}
|
||||
|
||||
private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
||||
case name(placeholder: String, value: String)
|
||||
case filterPrivateChats(title: String, value: Bool)
|
||||
case filterSecretChats(title: String, value: Bool)
|
||||
case filterPrivateGroups(title: String, value: Bool)
|
||||
case filterBots(title: String, value: Bool)
|
||||
case filterPublicGroups(title: String, value: Bool)
|
||||
case filterChannels(title: String, value: Bool)
|
||||
case filterMuted(title: String, value: Bool)
|
||||
case filterRead(title: String, value: Bool)
|
||||
case additionalPeersHeader(String)
|
||||
case addAdditionalPeer(title: String)
|
||||
case additionalPeer(index: Int, peer: RenderedPeer, isRevealed: Bool)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .name:
|
||||
return ChatListFilterPresetControllerSection.name.rawValue
|
||||
case .filterPrivateChats, .filterSecretChats, .filterPrivateGroups, .filterBots, .filterPublicGroups, .filterChannels:
|
||||
return ChatListFilterPresetControllerSection.categories.rawValue
|
||||
case .filterMuted, .filterRead:
|
||||
return ChatListFilterPresetControllerSection.excludeCategories.rawValue
|
||||
case .additionalPeersHeader, .addAdditionalPeer, .additionalPeer:
|
||||
return ChatListFilterPresetControllerSection.additionalPeers.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: ChatListFilterPresetEntryStableId {
|
||||
switch self {
|
||||
case .name:
|
||||
return .index(0)
|
||||
case .filterPrivateChats:
|
||||
return .index(1)
|
||||
case .filterSecretChats:
|
||||
return .index(2)
|
||||
case .filterPrivateGroups:
|
||||
return .index(3)
|
||||
case .filterBots:
|
||||
return .index(4)
|
||||
case .filterPublicGroups:
|
||||
return .index(5)
|
||||
case .filterChannels:
|
||||
return .index(6)
|
||||
case .filterMuted:
|
||||
return .index(7)
|
||||
case .filterRead:
|
||||
return .index(8)
|
||||
case .additionalPeersHeader:
|
||||
return .index(9)
|
||||
case .addAdditionalPeer:
|
||||
return .index(10)
|
||||
case let .additionalPeer(additionalPeer):
|
||||
return .peer(additionalPeer.peer.peerId)
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: ChatListFilterPresetEntry, rhs: ChatListFilterPresetEntry) -> Bool {
|
||||
switch lhs.stableId {
|
||||
case let .index(lhsIndex):
|
||||
switch rhs.stableId {
|
||||
case let .index(rhsIndex):
|
||||
return lhsIndex < rhsIndex
|
||||
case .peer:
|
||||
return true
|
||||
}
|
||||
case .peer:
|
||||
switch lhs {
|
||||
case let .additionalPeer(lhsIndex, _, _):
|
||||
switch rhs.stableId {
|
||||
case .index:
|
||||
return false
|
||||
case .peer:
|
||||
switch rhs {
|
||||
case let .additionalPeer(rhsIndex, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! ChatListFilterPresetControllerArguments
|
||||
switch self {
|
||||
case let .name(placeholder, value):
|
||||
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), sectionId: self.section, textUpdated: { value in
|
||||
arguments.updateState { current in
|
||||
var state = current
|
||||
state.name = value
|
||||
return state
|
||||
}
|
||||
}, action: {})
|
||||
case let .filterPrivateChats(title, value):
|
||||
return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .privateChats, section: self.section)
|
||||
case let .filterSecretChats(title, value):
|
||||
return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .secretChats, section: self.section)
|
||||
case let .filterPrivateGroups(title, value):
|
||||
return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .privateGroups, section: self.section)
|
||||
case let .filterBots(title, value):
|
||||
return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .bots, section: self.section)
|
||||
case let .filterPublicGroups(title, value):
|
||||
return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .publicGroups, section: self.section)
|
||||
case let .filterChannels(title, value):
|
||||
return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .channels, section: self.section)
|
||||
case let .filterMuted(title, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { _ in
|
||||
arguments.updateState { current in
|
||||
var state = current
|
||||
if state.includeCategories.contains(.muted) {
|
||||
state.includeCategories.remove(.muted)
|
||||
} else {
|
||||
state.includeCategories.insert(.muted)
|
||||
}
|
||||
return state
|
||||
}
|
||||
})
|
||||
case let .filterRead(title, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { _ in
|
||||
arguments.updateState { current in
|
||||
var state = current
|
||||
if state.includeCategories.contains(.read) {
|
||||
state.includeCategories.remove(.read)
|
||||
} else {
|
||||
state.includeCategories.insert(.read)
|
||||
}
|
||||
return state
|
||||
}
|
||||
})
|
||||
case let .additionalPeersHeader(title):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
|
||||
case let .addAdditionalPeer(title):
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(presentationData.theme), title: title, alwaysPlain: false, sectionId: self.section, height: .peerList, editing: false, action: {
|
||||
arguments.openAddPeer()
|
||||
})
|
||||
case let .additionalPeer(title, peer, isRevealed):
|
||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.chatMainPeer!, height: .peerList, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: isRevealed), revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: {
|
||||
arguments.deleteAdditionalPeer(peer.peerId)
|
||||
})]), switchValue: nil, enabled: true, selectable: false, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in
|
||||
arguments.setPeerIdWithRevealedOptions(lhs, rhs)
|
||||
}, removePeer: { id in
|
||||
arguments.deleteAdditionalPeer(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ChatListFilterPresetControllerState: Equatable {
|
||||
var name: String
|
||||
var includeCategories: ChatListIncludeCategoryFilter
|
||||
var additionallyIncludePeers: [PeerId]
|
||||
|
||||
var revealedPeerId: PeerId?
|
||||
|
||||
var isComplete: Bool {
|
||||
if self.name.isEmpty {
|
||||
return false
|
||||
}
|
||||
if self.includeCategories.isEmpty && self.additionallyIncludePeers.isEmpty {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func chatListFilterPresetControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetControllerState, peers: [RenderedPeer]) -> [ChatListFilterPresetEntry] {
|
||||
var entries: [ChatListFilterPresetEntry] = []
|
||||
|
||||
entries.append(.name(placeholder: "Preset Name", value: state.name))
|
||||
|
||||
entries.append(.filterPrivateChats(title: "Private Chats", value: state.includeCategories.contains(.privateChats)))
|
||||
entries.append(.filterSecretChats(title: "Secret Chats", value: state.includeCategories.contains(.secretChats)))
|
||||
entries.append(.filterPrivateGroups(title: "Private Groups", value: state.includeCategories.contains(.privateGroups)))
|
||||
entries.append(.filterBots(title: "Bots", value: state.includeCategories.contains(.bots)))
|
||||
entries.append(.filterPublicGroups(title: "Public Groups", value: state.includeCategories.contains(.publicGroups)))
|
||||
entries.append(.filterChannels(title: "Channels", value: state.includeCategories.contains(.channels)))
|
||||
|
||||
entries.append(.filterMuted(title: "Exclude Muted", value: !state.includeCategories.contains(.muted)))
|
||||
entries.append(.filterRead(title: "Exclude Read", value: !state.includeCategories.contains(.read)))
|
||||
|
||||
entries.append(.additionalPeersHeader("ALWAYS INCLUDE"))
|
||||
entries.append(.addAdditionalPeer(title: "Add"))
|
||||
|
||||
for peer in peers {
|
||||
entries.append(.additionalPeer(index: entries.count, peer: peer, isRevealed: state.revealedPeerId == peer.peerId))
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func chatListFilterPresetController(context: AccountContext, currentPreset: ChatListFilterPreset?) -> ViewController {
|
||||
let initialName: String
|
||||
if let currentPreset = currentPreset {
|
||||
switch currentPreset.name {
|
||||
case .unread:
|
||||
initialName = "Unread"
|
||||
case let .custom(value):
|
||||
initialName = value
|
||||
}
|
||||
} else {
|
||||
initialName = "New Preset"
|
||||
}
|
||||
let initialState = ChatListFilterPresetControllerState(name: initialName, includeCategories: currentPreset?.includeCategories ?? .all, additionallyIncludePeers: currentPreset?.additionallyIncludePeers ?? [])
|
||||
let stateValue = Atomic(value: initialState)
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let addPeerDisposable = MetaDisposable()
|
||||
actionsDisposable.add(addPeerDisposable)
|
||||
|
||||
var presentControllerImpl: ((ViewController, Any?) -> Void)?
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let arguments = ChatListFilterPresetControllerArguments(
|
||||
context: context,
|
||||
updateState: { f in
|
||||
updateState(f)
|
||||
},
|
||||
openAddPeer: {
|
||||
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true), options: []))
|
||||
addPeerDisposable.set((controller.result
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak controller] peerIds in
|
||||
controller?.dismiss()
|
||||
updateState { state in
|
||||
var state = state
|
||||
for peerId in peerIds {
|
||||
switch peerId {
|
||||
case let .peer(id):
|
||||
if !state.additionallyIncludePeers.contains(id) {
|
||||
state.additionallyIncludePeers.append(id)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
}))
|
||||
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
},
|
||||
deleteAdditionalPeer: { peerId in
|
||||
updateState { state in
|
||||
var state = state
|
||||
if let index = state.additionallyIncludePeers.index(of: peerId) {
|
||||
state.additionallyIncludePeers.remove(at: index)
|
||||
}
|
||||
return state
|
||||
}
|
||||
},
|
||||
setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
||||
updateState { state in
|
||||
var state = state
|
||||
if (peerId == nil && fromPeerId == state.revealedPeerId) || (peerId != nil && fromPeerId == nil) {
|
||||
state.revealedPeerId = peerId
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let statePeers = statePromise.get()
|
||||
|> map { state -> [PeerId] in
|
||||
return state.additionallyIncludePeers
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { peerIds -> Signal<[RenderedPeer], NoError> in
|
||||
return context.account.postbox.transaction { transaction -> [RenderedPeer] in
|
||||
var result: [RenderedPeer] = []
|
||||
for peerId in peerIds {
|
||||
if let peer = transaction.getPeer(peerId) {
|
||||
result.append(RenderedPeer(peer: peer))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get(),
|
||||
statePeers
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state, statePeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
||||
dismissImpl?()
|
||||
})
|
||||
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: {
|
||||
let state = stateValue.with { $0 }
|
||||
let preset = ChatListFilterPreset(name: .custom(state.name), includeCategories: state.includeCategories, additionallyIncludePeers: state.additionallyIncludePeers)
|
||||
let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in
|
||||
var settings = settings
|
||||
settings.presets = settings.presets.filter { $0 != preset && $0 != currentPreset }
|
||||
settings.presets.append(preset)
|
||||
return settings
|
||||
})
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
dismissImpl?()
|
||||
})
|
||||
})
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, state: state, peers: statePeers), style: .blocks, emptyStateItem: nil, animateChanges: true)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
controller.navigationPresentation = .modal
|
||||
presentControllerImpl = { [weak controller] c, d in
|
||||
controller?.present(c, in: .window(.root), with: d)
|
||||
}
|
||||
dismissImpl = { [weak controller] in
|
||||
let _ = controller?.dismiss()
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
@ -0,0 +1,212 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
|
||||
private final class ChatListFilterPresetListControllerArguments {
|
||||
let context: AccountContext
|
||||
|
||||
let openPreset: (ChatListFilterPreset) -> Void
|
||||
let addNew: () -> Void
|
||||
let setItemWithRevealedOptions: (ChatListFilterPreset?, ChatListFilterPreset?) -> Void
|
||||
let removePreset: (ChatListFilterPreset) -> Void
|
||||
|
||||
init(context: AccountContext, openPreset: @escaping (ChatListFilterPreset) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (ChatListFilterPreset?, ChatListFilterPreset?) -> Void, removePreset: @escaping (ChatListFilterPreset) -> Void) {
|
||||
self.context = context
|
||||
self.openPreset = openPreset
|
||||
self.addNew = addNew
|
||||
self.setItemWithRevealedOptions = setItemWithRevealedOptions
|
||||
self.removePreset = removePreset
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChatListFilterPresetListSection: Int32 {
|
||||
case list
|
||||
}
|
||||
|
||||
private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
|
||||
if peers.isEmpty {
|
||||
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
|
||||
} else {
|
||||
var result = 0
|
||||
for (_, peer) in peers {
|
||||
result += peer.userCount
|
||||
}
|
||||
return strings.UserCount(Int32(result))
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChatListFilterPresetListEntryStableId: Hashable {
|
||||
case listHeader
|
||||
case preset(ChatListFilterPresetName)
|
||||
case addItem
|
||||
case listFooter
|
||||
}
|
||||
|
||||
private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
|
||||
case listHeader(String)
|
||||
case preset(index: Int, title: String, preset: ChatListFilterPreset, canBeReordered: Bool, canBeDeleted: Bool)
|
||||
case addItem(String)
|
||||
case listFooter(String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .listHeader, .preset, .addItem, .listFooter:
|
||||
return ChatListFilterPresetListSection.list.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var sortId: Int {
|
||||
switch self {
|
||||
case .listHeader:
|
||||
return 0
|
||||
case let .preset(preset):
|
||||
return 1 + preset.index
|
||||
case .addItem:
|
||||
return 1000
|
||||
case .listFooter:
|
||||
return 1001
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: ChatListFilterPresetListEntryStableId {
|
||||
switch self {
|
||||
case .listHeader:
|
||||
return .listHeader
|
||||
case let .preset(preset):
|
||||
return .preset(preset.preset.name)
|
||||
case .addItem:
|
||||
return .addItem
|
||||
case .listFooter:
|
||||
return .listFooter
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: ChatListFilterPresetListEntry, rhs: ChatListFilterPresetListEntry) -> Bool {
|
||||
return lhs.sortId < rhs.sortId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! ChatListFilterPresetListControllerArguments
|
||||
switch self {
|
||||
case let .listHeader(text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
|
||||
case let .preset(index, title, preset, canBeReordered, canBeDeleted):
|
||||
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, editing: ChatListFilterPresetListItemEditing(editable: true, editing: false, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, sectionId: self.section, action: {
|
||||
arguments.openPreset(preset)
|
||||
}, setItemWithRevealedOptions: { lhs, rhs in
|
||||
arguments.setItemWithRevealedOptions(lhs, rhs)
|
||||
}, remove: {
|
||||
arguments.removePreset(preset)
|
||||
})
|
||||
case let .addItem(text):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.addNew()
|
||||
})
|
||||
case let .listFooter(text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ChatListFilterPresetListControllerState: Equatable {
|
||||
var revealedPreset: ChatListFilterPreset? = nil
|
||||
}
|
||||
|
||||
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, settings: ChatListFilterSettings) -> [ChatListFilterPresetListEntry] {
|
||||
var entries: [ChatListFilterPresetListEntry] = []
|
||||
|
||||
entries.append(.listHeader("PRESETS"))
|
||||
for preset in settings.presets {
|
||||
let title: String
|
||||
switch preset.name {
|
||||
case .unread:
|
||||
title = "Unread"
|
||||
case let .custom(value):
|
||||
title = value
|
||||
}
|
||||
entries.append(.preset(index: entries.count, title: title, preset: preset, canBeReordered: settings.presets.count > 1, canBeDeleted: true))
|
||||
}
|
||||
entries.append(.addItem("Add New"))
|
||||
entries.append(.listFooter("Add custom presets"))
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func chatListFilterPresetListController(context: AccountContext) -> ViewController {
|
||||
let initialState = ChatListFilterPresetListControllerState()
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
let updateState: ((ChatListFilterPresetListControllerState) -> ChatListFilterPresetListControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
var presentControllerImpl: ((ViewController, Any?) -> Void)?
|
||||
|
||||
let arguments = ChatListFilterPresetListControllerArguments(context: context, openPreset: { preset in
|
||||
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset))
|
||||
}, addNew: {
|
||||
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil))
|
||||
}, setItemWithRevealedOptions: { preset, fromPreset in
|
||||
updateState { state in
|
||||
var state = state
|
||||
if (preset == nil && fromPreset == state.revealedPreset) || (preset != nil && fromPreset == nil) {
|
||||
state.revealedPreset = preset
|
||||
}
|
||||
return state
|
||||
}
|
||||
}, removePreset: { preset in
|
||||
let _ = updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in
|
||||
var settings = settings
|
||||
if let index = settings.presets.index(of: preset) {
|
||||
settings.presets.remove(at: index)
|
||||
}
|
||||
return settings
|
||||
}).start()
|
||||
})
|
||||
|
||||
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get(),
|
||||
preferences
|
||||
)
|
||||
|> map { presentationData, state, preferences -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let settings = preferences.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings] as? ChatListFilterSettings ?? ChatListFilterSettings.default
|
||||
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Close), style: .regular, enabled: true, action: {
|
||||
dismissImpl?()
|
||||
})
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Filter Presets"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, settings: settings), style: .blocks, animateChanges: true)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|> afterDisposed {
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
controller.navigationPresentation = .modal
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
controller?.push(c)
|
||||
}
|
||||
presentControllerImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
439
submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift
Normal file
439
submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift
Normal file
@ -0,0 +1,439 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import ActivityIndicator
|
||||
import TelegramUIPreferences
|
||||
|
||||
struct ChatListFilterPresetListItemEditing: Equatable {
|
||||
let editable: Bool
|
||||
let editing: Bool
|
||||
let revealed: Bool
|
||||
}
|
||||
|
||||
final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let preset: ChatListFilterPreset
|
||||
let title: String
|
||||
let editing: ChatListFilterPresetListItemEditing
|
||||
let canBeReordered: Bool
|
||||
let canBeDeleted: Bool
|
||||
let sectionId: ItemListSectionId
|
||||
let action: () -> Void
|
||||
let setItemWithRevealedOptions: (ChatListFilterPreset?, ChatListFilterPreset?) -> Void
|
||||
let remove: () -> Void
|
||||
|
||||
init(
|
||||
presentationData: ItemListPresentationData,
|
||||
preset: ChatListFilterPreset,
|
||||
title: String,
|
||||
editing: ChatListFilterPresetListItemEditing,
|
||||
canBeReordered: Bool,
|
||||
canBeDeleted: Bool,
|
||||
sectionId: ItemListSectionId,
|
||||
action: @escaping () -> Void,
|
||||
setItemWithRevealedOptions: @escaping (ChatListFilterPreset?, ChatListFilterPreset?) -> Void,
|
||||
remove: @escaping () -> Void
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.preset = preset
|
||||
self.title = title
|
||||
self.editing = editing
|
||||
self.canBeReordered = canBeReordered
|
||||
self.canBeDeleted = canBeDeleted
|
||||
self.sectionId = sectionId
|
||||
self.action = action
|
||||
self.setItemWithRevealedOptions = setItemWithRevealedOptions
|
||||
self.remove = remove
|
||||
}
|
||||
|
||||
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 = ChatListFilterPresetListItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply(false) })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if let nodeValue = node() as? ChatListFilterPresetListItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
var animated = true
|
||||
if case .None = animation {
|
||||
animated = false
|
||||
}
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply(animated)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = true
|
||||
|
||||
func selected(listView: ListView){
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let titleNode: TextNode
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var editableControlNode: ItemListEditableControlNode?
|
||||
private var reorderControlNode: ItemListEditableReorderControlNode?
|
||||
|
||||
private var item: ChatListFilterPresetListItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
override var canBeSelected: Bool {
|
||||
if self.editableControlNode != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
|
||||
self.activateArea.activate = { [weak self] in
|
||||
self?.item?.action()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ChatListFilterPresetListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
|
||||
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: PresentationTheme?
|
||||
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
updatedTheme = item.presentationData.theme
|
||||
}
|
||||
|
||||
let peerRevealOptions: [ItemListRevealOption]
|
||||
if item.editing.editable && item.canBeDeleted {
|
||||
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]
|
||||
} else {
|
||||
peerRevealOptions = []
|
||||
}
|
||||
|
||||
let titleAttributedString = NSMutableAttributedString()
|
||||
titleAttributedString.append(NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor))
|
||||
|
||||
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
|
||||
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
|
||||
|
||||
let editingOffset: CGFloat = 0.0
|
||||
var reorderInset: CGFloat = 0.0
|
||||
|
||||
if item.editing.editing && item.canBeReordered {
|
||||
/*let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
|
||||
editableControlSizeAndApply = sizeAndApply
|
||||
editingOffset = sizeAndApply.0*/
|
||||
|
||||
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
|
||||
reorderControlSizeAndApply = reorderSizeAndApply
|
||||
reorderInset = reorderSizeAndApply.0
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 16.0 + params.leftInset
|
||||
let rightInset: CGFloat = params.rightInset + max(reorderInset, 55.0)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + 11.0 * 2.0)
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] animated in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
strongSelf.activateArea.accessibilityLabel = "\(titleAttributedString.string))"
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
let revealOffset = strongSelf.revealOffset
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
if let editableControlSizeAndApply = editableControlSizeAndApply {
|
||||
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height))
|
||||
if strongSelf.editableControlNode == nil {
|
||||
let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height)
|
||||
editableControlNode.tapped = {
|
||||
if let strongSelf = self {
|
||||
strongSelf.setRevealOptionsOpened(true, animated: true)
|
||||
strongSelf.revealOptionsInteractivelyOpened()
|
||||
}
|
||||
}
|
||||
strongSelf.editableControlNode = editableControlNode
|
||||
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode)
|
||||
editableControlNode.frame = editableControlFrame
|
||||
transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY))
|
||||
editableControlNode.alpha = 0.0
|
||||
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
|
||||
} else {
|
||||
strongSelf.editableControlNode?.frame = editableControlFrame
|
||||
}
|
||||
strongSelf.editableControlNode?.isHidden = !item.editing.editable
|
||||
} else if let editableControlNode = strongSelf.editableControlNode {
|
||||
var editableControlFrame = editableControlNode.frame
|
||||
editableControlFrame.origin.x = -editableControlFrame.size.width
|
||||
strongSelf.editableControlNode = nil
|
||||
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
|
||||
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
|
||||
editableControlNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
|
||||
if strongSelf.reorderControlNode == nil {
|
||||
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
|
||||
strongSelf.reorderControlNode = reorderControlNode
|
||||
strongSelf.addSubnode(reorderControlNode)
|
||||
reorderControlNode.alpha = 0.0
|
||||
transition.updateAlpha(node: reorderControlNode, alpha: 1.0)
|
||||
}
|
||||
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height))
|
||||
strongSelf.reorderControlNode?.frame = reorderControlFrame
|
||||
} else if let reorderControlNode = strongSelf.reorderControlNode {
|
||||
strongSelf.reorderControlNode = nil
|
||||
transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in
|
||||
reorderControlNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset + editingOffset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
|
||||
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
|
||||
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size))
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
|
||||
strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
guard let params = self.layoutParams else {
|
||||
return
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 16.0 + params.leftInset
|
||||
|
||||
let editingOffset: CGFloat
|
||||
if let editableControlNode = self.editableControlNode {
|
||||
editingOffset = editableControlNode.bounds.size.width
|
||||
var editableControlFrame = editableControlNode.frame
|
||||
editableControlFrame.origin.x = params.leftInset + offset
|
||||
transition.updateFrame(node: editableControlNode, frame: editableControlFrame)
|
||||
} else {
|
||||
editingOffset = 0.0
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let item = self.item {
|
||||
item.setItemWithRevealedOptions(item.preset, nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyClosed() {
|
||||
if let item = self.item {
|
||||
item.setItemWithRevealedOptions(nil, item.preset)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
||||
self.setRevealOptionsOpened(false, animated: true)
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
|
||||
if let item = self.item {
|
||||
item.remove()
|
||||
}
|
||||
}
|
||||
|
||||
override func isReorderable(at point: CGPoint) -> Bool {
|
||||
if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
@ -366,14 +366,17 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
|
||||
private var currentLocation: ChatListNodeLocation?
|
||||
var chatListFilter: ChatListNodeFilter = .all {
|
||||
var chatListFilter: ChatListFilterPreset? {
|
||||
didSet {
|
||||
if self.chatListFilter != oldValue {
|
||||
self.chatListFilterValue.set(self.chatListFilter)
|
||||
}
|
||||
}
|
||||
}
|
||||
private let chatListFilterValue = ValuePromise<ChatListNodeFilter>(.all)
|
||||
private let chatListFilterValue = ValuePromise<ChatListFilterPreset?>(nil)
|
||||
var chatListFilterSignal: Signal<ChatListFilterPreset?, NoError> {
|
||||
return self.chatListFilterValue.get()
|
||||
}
|
||||
private let chatListLocation = ValuePromise<ChatListNodeLocation>()
|
||||
private let chatListDisposable = MetaDisposable()
|
||||
private var activityStatusesDisposable: Disposable?
|
||||
@ -540,7 +543,7 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
return true
|
||||
})
|
||||
|> mapToSignal { location, filter -> Signal<(ChatListNodeViewUpdate, ChatListNodeFilter), NoError> in
|
||||
|> mapToSignal { location, filter -> Signal<(ChatListNodeViewUpdate, ChatListFilterPreset?), NoError> in
|
||||
return chatListViewForLocation(groupId: groupId, filter: filter, location: location, account: context.account)
|
||||
|> map { update in
|
||||
return (update, filter)
|
||||
|
@ -4,6 +4,7 @@ import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramUIPreferences
|
||||
|
||||
enum ChatListNodeLocation: Equatable {
|
||||
case initial(count: Int)
|
||||
@ -31,35 +32,20 @@ struct ChatListNodeViewUpdate {
|
||||
let scrollPosition: ChatListNodeViewScrollPosition?
|
||||
}
|
||||
|
||||
struct ChatListNodeFilter: OptionSet {
|
||||
var rawValue: Int32
|
||||
|
||||
init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let muted = ChatListNodeFilter(rawValue: 1 << 1)
|
||||
static let privateChats = ChatListNodeFilter(rawValue: 1 << 2)
|
||||
static let groups = ChatListNodeFilter(rawValue: 1 << 3)
|
||||
static let bots = ChatListNodeFilter(rawValue: 1 << 4)
|
||||
static let channels = ChatListNodeFilter(rawValue: 1 << 5)
|
||||
|
||||
static let all: ChatListNodeFilter = [
|
||||
.muted,
|
||||
.privateChats,
|
||||
.groups,
|
||||
.bots,
|
||||
.channels
|
||||
]
|
||||
}
|
||||
|
||||
func chatListViewForLocation(groupId: PeerGroupId, filter: ChatListNodeFilter, location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> {
|
||||
let filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?
|
||||
if filter == .all {
|
||||
filterPredicate = nil
|
||||
} else {
|
||||
filterPredicate = { peer, notificationSettings in
|
||||
if !filter.contains(.muted) {
|
||||
func chatListViewForLocation(groupId: PeerGroupId, filter: ChatListFilterPreset?, location: ChatListNodeLocation, account: Account) -> Signal<ChatListNodeViewUpdate, NoError> {
|
||||
let filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?
|
||||
if let filter = filter {
|
||||
let includePeers = Set(filter.additionallyIncludePeers)
|
||||
filterPredicate = { peer, notificationSettings, isUnread in
|
||||
if includePeers.contains(peer.id) {
|
||||
return true
|
||||
}
|
||||
if !filter.includeCategories.contains(.read) {
|
||||
if !isUnread {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !filter.includeCategories.contains(.muted) {
|
||||
if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
|
||||
if case .muted = notificationSettings.muteState {
|
||||
return false
|
||||
@ -68,32 +54,46 @@ func chatListViewForLocation(groupId: PeerGroupId, filter: ChatListNodeFilter, l
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !filter.contains(.privateChats) {
|
||||
if !filter.includeCategories.contains(.privateChats) {
|
||||
if let user = peer as? TelegramUser {
|
||||
if user.botInfo == nil {
|
||||
return false
|
||||
}
|
||||
} else if let _ = peer as? TelegramSecretChat {
|
||||
}
|
||||
}
|
||||
if !filter.includeCategories.contains(.secretChats) {
|
||||
if let _ = peer as? TelegramSecretChat {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !filter.contains(.bots) {
|
||||
if !filter.includeCategories.contains(.bots) {
|
||||
if let user = peer as? TelegramUser {
|
||||
if user.botInfo != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if !filter.contains(.groups) {
|
||||
if !filter.includeCategories.contains(.privateGroups) {
|
||||
if let _ = peer as? TelegramGroup {
|
||||
return false
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if case .group = channel.info {
|
||||
return false
|
||||
if channel.username == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !filter.contains(.channels) {
|
||||
if !filter.includeCategories.contains(.publicGroups) {
|
||||
if let channel = peer as? TelegramChannel {
|
||||
if case .group = channel.info {
|
||||
if channel.username != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !filter.includeCategories.contains(.channels) {
|
||||
if let channel = peer as? TelegramChannel {
|
||||
if case .broadcast = channel.info {
|
||||
return false
|
||||
@ -102,6 +102,8 @@ func chatListViewForLocation(groupId: PeerGroupId, filter: ChatListNodeFilter, l
|
||||
}
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
filterPredicate = nil
|
||||
}
|
||||
|
||||
switch location {
|
||||
|
@ -6,12 +6,13 @@ import SwiftSignalKit
|
||||
import Display
|
||||
import MergeLists
|
||||
import SearchUI
|
||||
import TelegramUIPreferences
|
||||
|
||||
struct ChatListNodeView {
|
||||
let originalView: ChatListView
|
||||
let filteredEntries: [ChatListNodeEntry]
|
||||
let isLoading: Bool
|
||||
let filter: ChatListNodeFilter
|
||||
let filter: ChatListFilterPreset?
|
||||
}
|
||||
|
||||
enum ChatListNodeViewTransitionReason {
|
||||
|
@ -5,32 +5,39 @@ import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import SyncCore
|
||||
import Postbox
|
||||
import TelegramUIPreferences
|
||||
|
||||
final class TabBarChatListFilterController: ViewController {
|
||||
private var controllerNode: TabBarChatListFilterControllerNode {
|
||||
return self.displayNode as! TabBarChatListFilterControllerNode
|
||||
}
|
||||
|
||||
private let _ready = Promise<Bool>(true)
|
||||
private let _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let sourceNodes: [ASDisplayNode]
|
||||
private let currentFilter: ChatListNodeFilter
|
||||
private let updateFilter: (ChatListNodeFilter) -> Void
|
||||
private let presetList: [ChatListFilterPreset]
|
||||
private let currentPreset: ChatListFilterPreset?
|
||||
private let setup: () -> Void
|
||||
private let updatePreset: (ChatListFilterPreset?) -> Void
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var didPlayPresentationAnimation = false
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
public init(context: AccountContext, sourceNodes: [ASDisplayNode], currentFilter: ChatListNodeFilter, updateFilter: @escaping (ChatListNodeFilter) -> Void) {
|
||||
public init(context: AccountContext, sourceNodes: [ASDisplayNode], presetList: [ChatListFilterPreset], currentPreset: ChatListFilterPreset?, setup: @escaping () -> Void, updatePreset: @escaping (ChatListFilterPreset?) -> Void) {
|
||||
self.context = context
|
||||
self.sourceNodes = sourceNodes
|
||||
self.currentFilter = currentFilter
|
||||
self.updateFilter = updateFilter
|
||||
self.presetList = presetList
|
||||
self.currentPreset = currentPreset
|
||||
self.setup = setup
|
||||
self.updatePreset = updatePreset
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
@ -52,7 +59,14 @@ final class TabBarChatListFilterController: ViewController {
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = TabBarChatListFilterControllerNode(context: self.context, presentationData: self.presentationData, cancel: { [weak self] in
|
||||
self?.dismiss()
|
||||
}, sourceNodes: self.sourceNodes, currentFilter: self.currentFilter, updateFilter: self.updateFilter)
|
||||
}, sourceNodes: self.sourceNodes, presetList: self.presetList, currentPreset: self.currentPreset, setup: { [weak self] in
|
||||
self?.setup()
|
||||
self?.dismiss(sourceNodes: [], fadeOutIcon: true)
|
||||
}, updatePreset: { [weak self] filter in
|
||||
self?.updatePreset(filter)
|
||||
self?.dismiss()
|
||||
})
|
||||
self._ready.set(self.controllerNode.isReady.get())
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
@ -74,11 +88,11 @@ final class TabBarChatListFilterController: ViewController {
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.dismiss(sourceNodes: [])
|
||||
self.dismiss(sourceNodes: [], fadeOutIcon: false)
|
||||
}
|
||||
|
||||
public func dismiss(sourceNodes: [ASDisplayNode]) {
|
||||
self.controllerNode.animateOut(sourceNodes: sourceNodes, completion: { [weak self] in
|
||||
func dismiss(sourceNodes: [ASDisplayNode], fadeOutIcon: Bool) {
|
||||
self.controllerNode.animateOut(sourceNodes: sourceNodes, fadeOutIcon: fadeOutIcon, completion: { [weak self] in
|
||||
self?.didPlayPresentationAnimation = false
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
})
|
||||
@ -91,9 +105,86 @@ private protocol AbstractTabBarChatListFilterItemNode {
|
||||
func updateLayout(maxWidth: CGFloat) -> (CGFloat, CGFloat, (CGFloat) -> Void)
|
||||
}
|
||||
|
||||
private final class AddFilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterItemNode {
|
||||
private let action: () -> Void
|
||||
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
private let plusNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
init(displaySeparator: Bool, presentationData: PresentationData, action: @escaping () -> Void) {
|
||||
self.action = action
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor
|
||||
self.separatorNode.isHidden = !displaySeparator
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.attributedText = NSAttributedString(string: "Setup", font: Font.regular(17.0), textColor: presentationData.theme.actionSheet.primaryTextColor)
|
||||
|
||||
self.plusNode = ASImageNode()
|
||||
self.plusNode.image = generateItemListPlusIcon(presentationData.theme.actionSheet.primaryTextColor)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.plusNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(maxWidth: CGFloat) -> (CGFloat, CGFloat, (CGFloat) -> Void) {
|
||||
let leftInset: CGFloat = 16.0
|
||||
let rightInset: CGFloat = 10.0
|
||||
let iconInset: CGFloat = 60.0
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: maxWidth - leftInset - rightInset, height: .greatestFiniteMagnitude))
|
||||
let height: CGFloat = 61.0
|
||||
|
||||
return (titleSize.width + leftInset + rightInset, height, { width in
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
|
||||
if let image = self.plusNode.image {
|
||||
self.plusNode.frame = CGRect(origin: CGPoint(x: floor(width - iconInset + (iconInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
|
||||
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))
|
||||
self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height))
|
||||
self.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height))
|
||||
})
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private final class FilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterItemNode {
|
||||
private let context: AccountContext
|
||||
private let title: String
|
||||
let preset: ChatListFilterPreset?
|
||||
private let isCurrent: Bool
|
||||
private let presentationData: PresentationData
|
||||
private let action: () -> Bool
|
||||
@ -106,10 +197,12 @@ private final class FilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterI
|
||||
|
||||
private let badgeBackgroundNode: ASImageNode
|
||||
private let badgeTitleNode: ImmediateTextNode
|
||||
private var badgeText: String = ""
|
||||
|
||||
init(context: AccountContext, title: String, isCurrent: Bool, displaySeparator: Bool, presentationData: PresentationData, action: @escaping () -> Bool) {
|
||||
init(context: AccountContext, title: String, preset: ChatListFilterPreset?, isCurrent: Bool, displaySeparator: Bool, presentationData: PresentationData, action: @escaping () -> Bool) {
|
||||
self.context = context
|
||||
self.title = title
|
||||
self.preset = preset
|
||||
self.isCurrent = isCurrent
|
||||
self.presentationData = presentationData
|
||||
self.action = action
|
||||
@ -130,7 +223,7 @@ private final class FilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterI
|
||||
|
||||
self.checkNode = ASImageNode()
|
||||
self.checkNode.image = generateItemListCheckIcon(color: presentationData.theme.actionSheet.primaryTextColor)
|
||||
self.checkNode.isHidden = !isCurrent
|
||||
self.checkNode.isHidden = true//!isCurrent
|
||||
|
||||
self.badgeBackgroundNode = ASImageNode()
|
||||
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: presentationData.theme.list.itemCheckColors.fillColor)
|
||||
@ -169,7 +262,7 @@ private final class FilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterI
|
||||
let badgeMinSize = self.badgeBackgroundNode.image?.size.width ?? 20.0
|
||||
let badgeSize = CGSize(width: max(badgeMinSize, badgeTitleSize.width + 12.0), height: badgeMinSize)
|
||||
|
||||
let rightInset: CGFloat = max(60.0, badgeSize.width + 40.0)
|
||||
let rightInset: CGFloat = max(20.0, badgeSize.width + 20.0)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: maxWidth - leftInset - rightInset, height: .greatestFiniteMagnitude))
|
||||
|
||||
@ -193,8 +286,20 @@ private final class FilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterI
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
let isCurrent = self.action()
|
||||
self.checkNode.isHidden = !isCurrent
|
||||
let _ = self.action()
|
||||
//self.checkNode.isHidden = !isCurrent
|
||||
}
|
||||
|
||||
func updateBadge(text: String) -> Bool {
|
||||
if text != self.badgeText {
|
||||
self.badgeText = text
|
||||
self.badgeTitleNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
self.badgeBackgroundNode.isHidden = text.isEmpty
|
||||
self.badgeTitleNode.isHidden = text.isEmpty
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,7 +320,11 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, cancel: @escaping () -> Void, sourceNodes: [ASDisplayNode], currentFilter: ChatListNodeFilter, updateFilter: @escaping (ChatListNodeFilter) -> Void) {
|
||||
private var countsDisposable: Disposable?
|
||||
let isReady = Promise<Bool>()
|
||||
private var didSetIsReady = false
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, cancel: @escaping () -> Void, sourceNodes: [ASDisplayNode], presetList: [ChatListFilterPreset], currentPreset: ChatListFilterPreset?, setup: @escaping () -> Void, updatePreset: @escaping (ChatListFilterPreset?) -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.cancel = cancel
|
||||
self.sourceNodes = sourceNodes
|
||||
@ -245,30 +354,28 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
|
||||
self.contentContainerNode.clipsToBounds = true
|
||||
|
||||
var contentNodes: [ASDisplayNode & AbstractTabBarChatListFilterItemNode] = []
|
||||
contentNodes.append(AddFilterItemNode(displaySeparator: true, presentationData: presentationData, action: {
|
||||
setup()
|
||||
}))
|
||||
|
||||
let labels: [(String, ChatListNodeFilter)] = [
|
||||
("Private Chats", .privateChats),
|
||||
("Groups", .groups),
|
||||
("Bots", .bots),
|
||||
("Channels", .channels),
|
||||
("Muted", .muted)
|
||||
]
|
||||
contentNodes.append(FilterItemNode(context: context, title: "All", preset: nil, isCurrent: currentPreset == nil, displaySeparator: !presetList.isEmpty, presentationData: presentationData, action: {
|
||||
updatePreset(nil)
|
||||
return false
|
||||
}))
|
||||
|
||||
var updatedFilter = currentFilter
|
||||
let toggleFilter: (ChatListNodeFilter) -> Void = { filter in
|
||||
if updatedFilter.contains(filter) {
|
||||
updatedFilter.remove(filter)
|
||||
} else {
|
||||
updatedFilter.insert(filter)
|
||||
for i in 0 ..< presetList.count {
|
||||
let preset = presetList[i]
|
||||
|
||||
let title: String
|
||||
switch preset.name {
|
||||
case .unread:
|
||||
title = "Unread"
|
||||
case let .custom(value):
|
||||
title = value
|
||||
}
|
||||
updateFilter(updatedFilter)
|
||||
}
|
||||
|
||||
for i in 0 ..< labels.count {
|
||||
let filter = labels[i].1
|
||||
contentNodes.append(FilterItemNode(context: context, title: labels[i].0, isCurrent: updatedFilter.contains(filter), displaySeparator: i != labels.count - 1, presentationData: presentationData, action: {
|
||||
toggleFilter(filter)
|
||||
return updatedFilter.contains(filter)
|
||||
contentNodes.append(FilterItemNode(context: context, title: title, preset: preset, isCurrent: currentPreset == preset, displaySeparator: i != presetList.count - 1, presentationData: presentationData, action: {
|
||||
updatePreset(preset)
|
||||
return false
|
||||
}))
|
||||
}
|
||||
self.contentNodes = contentNodes
|
||||
@ -281,6 +388,126 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
|
||||
self.contentNodes.forEach(self.contentContainerNode.addSubnode)
|
||||
|
||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
|
||||
var unreadCountItems: [UnreadMessageCountsItem] = []
|
||||
unreadCountItems.append(.total(nil))
|
||||
var additionalPeerIds = Set<PeerId>()
|
||||
for preset in presetList {
|
||||
additionalPeerIds.formUnion(preset.additionallyIncludePeers)
|
||||
}
|
||||
if !additionalPeerIds.isEmpty {
|
||||
for peerId in additionalPeerIds {
|
||||
unreadCountItems.append(.peer(peerId))
|
||||
}
|
||||
}
|
||||
let unreadKey: PostboxViewKey = .unreadCounts(items: unreadCountItems)
|
||||
var keys: [PostboxViewKey] = []
|
||||
keys.append(unreadKey)
|
||||
for peerId in additionalPeerIds {
|
||||
keys.append(.basicPeer(peerId))
|
||||
}
|
||||
|
||||
self.countsDisposable = (context.account.postbox.combinedView(keys: keys)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] view in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let unreadCounts = view.views[unreadKey] as? UnreadMessageCountsView {
|
||||
var peerTagAndCount: [PeerId: (PeerSummaryCounterTags, Int)] = [:]
|
||||
|
||||
var totalState: ChatListTotalUnreadState?
|
||||
for entry in unreadCounts.entries {
|
||||
switch entry {
|
||||
case let .total(_, totalStateValue):
|
||||
totalState = totalStateValue
|
||||
case let .peer(peerId, state):
|
||||
if let state = state, state.isUnread {
|
||||
if let peerView = view.views[.basicPeer(peerId)] as? BasicPeerView, let peer = peerView.peer {
|
||||
let tag = context.account.postbox.seedConfiguration.peerSummaryCounterTags(peer)
|
||||
var peerCount = Int(state.count)
|
||||
if state.isUnread {
|
||||
peerCount = max(1, peerCount)
|
||||
}
|
||||
peerTagAndCount[peerId] = (tag, peerCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var totalUnreadChatCount = 0
|
||||
if let totalState = totalState {
|
||||
for (_, counters) in totalState.filteredCounters {
|
||||
totalUnreadChatCount += Int(counters.chatCount)
|
||||
}
|
||||
}
|
||||
|
||||
var shouldUpdateLayout = false
|
||||
for case let contentNode as FilterItemNode in strongSelf.contentNodes {
|
||||
let badgeString: String
|
||||
if let preset = contentNode.preset {
|
||||
var tags: [PeerSummaryCounterTags] = []
|
||||
if preset.includeCategories.contains(.privateChats) {
|
||||
tags.append(.privateChat)
|
||||
}
|
||||
if preset.includeCategories.contains(.secretChats) {
|
||||
tags.append(.secretChat)
|
||||
}
|
||||
if preset.includeCategories.contains(.privateGroups) {
|
||||
tags.append(.privateGroup)
|
||||
}
|
||||
if preset.includeCategories.contains(.bots) {
|
||||
tags.append(.bot)
|
||||
}
|
||||
if preset.includeCategories.contains(.publicGroups) {
|
||||
tags.append(.publicGroup)
|
||||
}
|
||||
if preset.includeCategories.contains(.privateChats) {
|
||||
tags.append(.channel)
|
||||
}
|
||||
|
||||
var count = 0
|
||||
if let totalState = totalState {
|
||||
for tag in tags {
|
||||
if preset.includeCategories.contains(.muted) {
|
||||
}
|
||||
if let value = totalState.filteredCounters[tag] {
|
||||
count += Int(value.chatCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
for peerId in preset.additionallyIncludePeers {
|
||||
if let (tag, peerCount) = peerTagAndCount[peerId] {
|
||||
if !tags.contains(tag) {
|
||||
count += peerCount
|
||||
}
|
||||
}
|
||||
}
|
||||
if count != 0 {
|
||||
badgeString = "\(count)"
|
||||
} else {
|
||||
badgeString = ""
|
||||
}
|
||||
} else {
|
||||
badgeString = ""
|
||||
}
|
||||
if contentNode.updateBadge(text: badgeString) {
|
||||
shouldUpdateLayout = true
|
||||
}
|
||||
}
|
||||
|
||||
if shouldUpdateLayout {
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !strongSelf.didSetIsReady {
|
||||
strongSelf.didSetIsReady = true
|
||||
strongSelf.isReady.set(.single(true))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -290,6 +517,8 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
|
||||
propertyAnimator?.stopAnimation(true)
|
||||
}
|
||||
}
|
||||
|
||||
self.countsDisposable?.dispose()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
@ -344,7 +573,7 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(sourceNodes: [ASDisplayNode], completion: @escaping () -> Void) {
|
||||
func animateOut(sourceNodes: [ASDisplayNode], fadeOutIcon: Bool, completion: @escaping () -> Void) {
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
var completedEffect = false
|
||||
@ -408,7 +637,14 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
|
||||
let sourceFrame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
|
||||
self.contentContainerNode.layer.animateFrame(from: self.contentContainerNode.frame, to: sourceFrame, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false)
|
||||
}
|
||||
completedSourceNodes = true
|
||||
if fadeOutIcon {
|
||||
for snapshotView in self.snapshotViews {
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
completedSourceNodes = true
|
||||
} else {
|
||||
completedSourceNodes = true
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
@ -420,7 +656,7 @@ private final class TabBarChatListFilterControllerNode: ViewControllerTracingNod
|
||||
let sideInset: CGFloat = 18.0
|
||||
|
||||
var contentSize = CGSize()
|
||||
contentSize.width = min(layout.size.width - 60.0, 220.0)
|
||||
contentSize.width = min(layout.size.width - 40.0, 260.0)
|
||||
var applyNodes: [(ASDisplayNode, CGFloat, (CGFloat) -> Void)] = []
|
||||
for itemNode in self.contentNodes {
|
||||
let (width, height, apply) = itemNode.updateLayout(maxWidth: contentSize.width - sideInset * 2.0)
|
||||
|
@ -297,7 +297,7 @@ private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [Pe
|
||||
|
||||
final class MutableChatListView {
|
||||
let groupId: PeerGroupId
|
||||
let filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?
|
||||
let filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?
|
||||
private let summaryComponents: ChatListEntrySummaryComponents
|
||||
fileprivate var additionalItemIds: Set<PeerId>
|
||||
fileprivate var additionalItemEntries: [MutableChatListEntry]
|
||||
@ -307,7 +307,7 @@ final class MutableChatListView {
|
||||
fileprivate var groupEntries: [ChatListGroupReferenceEntry]
|
||||
private var count: Int
|
||||
|
||||
init(postbox: Postbox, groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?, aroundIndex: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents) {
|
||||
init(postbox: Postbox, groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?, aroundIndex: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents) {
|
||||
let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: groupId, index: aroundIndex, count: count, filterPredicate: filterPredicate)
|
||||
|
||||
self.groupId = groupId
|
||||
@ -496,8 +496,9 @@ final class MutableChatListView {
|
||||
if let filterPredicate = self.filterPredicate {
|
||||
for (peerId, settingsChange) in updatedPeerNotificationSettings {
|
||||
if let peer = postbox.peerTable.get(peerId) {
|
||||
let wasIncluded = filterPredicate(peer, settingsChange.0)
|
||||
let isIncluded = filterPredicate(peer, settingsChange.1)
|
||||
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
|
||||
let wasIncluded = filterPredicate(peer, settingsChange.0, isUnread)
|
||||
let isIncluded = filterPredicate(peer, settingsChange.1, isUnread)
|
||||
if wasIncluded != isIncluded {
|
||||
if isIncluded {
|
||||
if let entry = postbox.chatListTable.getEntry(groupId: self.groupId, peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
|
||||
@ -664,7 +665,8 @@ final class MutableChatListView {
|
||||
switch initialEntry {
|
||||
case .IntermediateMessageEntry(let index, _, _, _), .MessageEntry(let index, _, _, _, _, _, _, _, _):
|
||||
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
|
||||
if !filterPredicate(peer, postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId)) {
|
||||
let isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
|
||||
if !filterPredicate(peer, postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
|
@ -1,8 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
struct PeerIdAndNamespace: Hashable {
|
||||
let peerId: PeerId
|
||||
let namespace: MessageId.Namespace
|
||||
public struct PeerIdAndNamespace: Hashable {
|
||||
public let peerId: PeerId
|
||||
public let namespace: MessageId.Namespace
|
||||
|
||||
public init(peerId: PeerId, namespace: MessageId.Namespace) {
|
||||
self.peerId = peerId
|
||||
self.namespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
private func canContainHoles(_ peerIdAndNamespace: PeerIdAndNamespace, seedConfiguration: SeedConfiguration) -> Bool {
|
||||
|
@ -1362,11 +1362,14 @@ public final class Postbox {
|
||||
print("(Postbox initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
|
||||
let _ = self.transaction({ transaction -> Void in
|
||||
let reindexUnreadVersion: Int32 = 1
|
||||
let reindexUnreadVersion: Int32 = 2
|
||||
if self.messageHistoryMetadataTable.getShouldReindexUnreadCountsState() != reindexUnreadVersion {
|
||||
self.messageHistoryMetadataTable.setShouldReindexUnreadCounts(value: true)
|
||||
self.messageHistoryMetadataTable.setShouldReindexUnreadCountsState(value: reindexUnreadVersion)
|
||||
}
|
||||
#if DEBUG
|
||||
self.messageHistoryMetadataTable.setShouldReindexUnreadCounts(value: true)
|
||||
#endif
|
||||
|
||||
if self.messageHistoryMetadataTable.shouldReindexUnreadCounts() {
|
||||
self.groupMessageStatsTable.removeAll()
|
||||
@ -1650,12 +1653,13 @@ public final class Postbox {
|
||||
self.synchronizeGroupMessageStatsTable.set(groupId: groupId, namespace: namespace, needsValidation: false, operations: &self.currentUpdatedGroupSummarySynchronizeOperations)
|
||||
}
|
||||
|
||||
private func mappedChatListFilterPredicate(_ predicate: @escaping (Peer, PeerNotificationSettings?) -> Bool) -> (ChatListIntermediateEntry) -> Bool {
|
||||
private func mappedChatListFilterPredicate(_ predicate: @escaping (Peer, PeerNotificationSettings?, Bool) -> Bool) -> (ChatListIntermediateEntry) -> Bool {
|
||||
return { entry in
|
||||
switch entry {
|
||||
case let .message(index, _, _):
|
||||
if let peer = self.peerTable.get(index.messageIndex.id.peerId) {
|
||||
if predicate(peer, self.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId)) {
|
||||
let isUnread = self.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
|
||||
if predicate(peer, self.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -1669,7 +1673,7 @@ public final class Postbox {
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAroundChatEntries(groupId: PeerGroupId, index: ChatListIndex, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?) -> (entries: [MutableChatListEntry], earlier: MutableChatListEntry?, later: MutableChatListEntry?) {
|
||||
func fetchAroundChatEntries(groupId: PeerGroupId, index: ChatListIndex, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?) -> (entries: [MutableChatListEntry], earlier: MutableChatListEntry?, later: MutableChatListEntry?) {
|
||||
let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate)
|
||||
let (intermediateEntries, intermediateLower, intermediateUpper) = self.chatListTable.entriesAround(groupId: groupId, index: index, messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
|
||||
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
|
||||
@ -1685,7 +1689,7 @@ public final class Postbox {
|
||||
return (entries, lower, upper)
|
||||
}
|
||||
|
||||
func fetchEarlierChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?) -> [MutableChatListEntry] {
|
||||
func fetchEarlierChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?) -> [MutableChatListEntry] {
|
||||
let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate)
|
||||
let intermediateEntries = self.chatListTable.earlierEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
|
||||
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
|
||||
@ -1694,7 +1698,7 @@ public final class Postbox {
|
||||
return entries
|
||||
}
|
||||
|
||||
func fetchLaterChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)?) -> [MutableChatListEntry] {
|
||||
func fetchLaterChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?) -> [MutableChatListEntry] {
|
||||
let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate)
|
||||
let intermediateEntries = self.chatListTable.laterEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
|
||||
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
|
||||
@ -2542,11 +2546,11 @@ public final class Postbox {
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
public func tailChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)? = nil, count: Int, summaryComponents: ChatListEntrySummaryComponents) -> Signal<(ChatListView, ViewUpdateType), NoError> {
|
||||
public func tailChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? = nil, count: Int, summaryComponents: ChatListEntrySummaryComponents) -> Signal<(ChatListView, ViewUpdateType), NoError> {
|
||||
return self.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: ChatListIndex.absoluteUpperBound, count: count, summaryComponents: summaryComponents, userInteractive: true)
|
||||
}
|
||||
|
||||
public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)? = nil, index: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents, userInteractive: Bool = false) -> Signal<(ChatListView, ViewUpdateType), NoError> {
|
||||
public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? = nil, index: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents, userInteractive: Bool = false) -> Signal<(ChatListView, ViewUpdateType), NoError> {
|
||||
return self.transactionSignal(userInteractive: userInteractive, { subscriber, transaction in
|
||||
let mutableView = MutableChatListView(postbox: self, groupId: groupId, filterPredicate: filterPredicate, aroundIndex: index, count: count, summaryComponents: summaryComponents)
|
||||
mutableView.render(postbox: self, renderMessage: self.renderIntermediateMessage, getPeer: { id in
|
||||
|
@ -17,6 +17,49 @@ import TelegramNotices
|
||||
import NotificationSoundSelectionUI
|
||||
import TelegramStringFormatting
|
||||
|
||||
private struct CounterTagSettings: OptionSet {
|
||||
var rawValue: Int32
|
||||
|
||||
init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
init(summaryTags: PeerSummaryCounterTags) {
|
||||
var result = CounterTagSettings()
|
||||
if summaryTags.contains(.privateChat) {
|
||||
result.insert(.regularChatsAndPrivateGroups)
|
||||
}
|
||||
if summaryTags.contains(.channel) {
|
||||
result.insert(.channels)
|
||||
}
|
||||
if summaryTags.contains(.publicGroup) {
|
||||
result.insert(.publicGroups)
|
||||
}
|
||||
self = result
|
||||
}
|
||||
|
||||
func toSumaryTags() -> PeerSummaryCounterTags {
|
||||
var result = PeerSummaryCounterTags()
|
||||
if self.contains(.regularChatsAndPrivateGroups) {
|
||||
result.insert(.privateChat)
|
||||
result.insert(.secretChat)
|
||||
result.insert(.bot)
|
||||
result.insert(.privateGroup)
|
||||
}
|
||||
if self.contains(.publicGroups) {
|
||||
result.insert(.publicGroup)
|
||||
}
|
||||
if self.contains(.channels) {
|
||||
result.insert(.channel)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static let regularChatsAndPrivateGroups = CounterTagSettings(rawValue: 1 << 0)
|
||||
static let publicGroups = CounterTagSettings(rawValue: 1 << 1)
|
||||
static let channels = CounterTagSettings(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
private final class NotificationsAndSoundsArguments {
|
||||
let context: AccountContext
|
||||
let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void
|
||||
@ -43,7 +86,7 @@ private final class NotificationsAndSoundsArguments {
|
||||
let updateInAppPreviews: (Bool) -> Void
|
||||
|
||||
let updateDisplayNameOnLockscreen: (Bool) -> Void
|
||||
let updateIncludeTag: (PeerSummaryCounterTags, Bool) -> Void
|
||||
let updateIncludeTag: (CounterTagSettings, Bool) -> Void
|
||||
let updateTotalUnreadCountCategory: (Bool) -> Void
|
||||
|
||||
let updateJoinedNotifications: (Bool) -> Void
|
||||
@ -56,7 +99,7 @@ private final class NotificationsAndSoundsArguments {
|
||||
|
||||
let updateNotificationsFromAllAccounts: (Bool) -> Void
|
||||
|
||||
init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, authorizeNotifications: @escaping () -> Void, suppressWarning: @escaping () -> Void, updateMessageAlerts: @escaping (Bool) -> Void, updateMessagePreviews: @escaping (Bool) -> Void, updateMessageSound: @escaping (PeerMessageSound) -> Void, updateGroupAlerts: @escaping (Bool) -> Void, updateGroupPreviews: @escaping (Bool) -> Void, updateGroupSound: @escaping (PeerMessageSound) -> Void, updateChannelAlerts: @escaping (Bool) -> Void, updateChannelPreviews: @escaping (Bool) -> Void, updateChannelSound: @escaping (PeerMessageSound) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateIncludeTag: @escaping (PeerSummaryCounterTags, Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, updatedExceptionMode: @escaping(NotificationExceptionMode) -> Void, openAppSettings: @escaping () -> Void, updateJoinedNotifications: @escaping (Bool) -> Void, updateNotificationsFromAllAccounts: @escaping (Bool) -> Void) {
|
||||
init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, authorizeNotifications: @escaping () -> Void, suppressWarning: @escaping () -> Void, updateMessageAlerts: @escaping (Bool) -> Void, updateMessagePreviews: @escaping (Bool) -> Void, updateMessageSound: @escaping (PeerMessageSound) -> Void, updateGroupAlerts: @escaping (Bool) -> Void, updateGroupPreviews: @escaping (Bool) -> Void, updateGroupSound: @escaping (PeerMessageSound) -> Void, updateChannelAlerts: @escaping (Bool) -> Void, updateChannelPreviews: @escaping (Bool) -> Void, updateChannelSound: @escaping (PeerMessageSound) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateIncludeTag: @escaping (CounterTagSettings, Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, updatedExceptionMode: @escaping(NotificationExceptionMode) -> Void, openAppSettings: @escaping () -> Void, updateJoinedNotifications: @escaping (Bool) -> Void, updateNotificationsFromAllAccounts: @escaping (Bool) -> Void) {
|
||||
self.context = context
|
||||
self.presentController = presentController
|
||||
self.pushController = pushController
|
||||
@ -779,8 +822,11 @@ private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warn
|
||||
entries.append(.displayNamesOnLockscreenInfo(presentationData.theme, presentationData.strings.Notifications_DisplayNamesOnLockScreenInfoWithLink))
|
||||
|
||||
entries.append(.badgeHeader(presentationData.theme, presentationData.strings.Notifications_Badge.uppercased()))
|
||||
entries.append(.includePublicGroups(presentationData.theme, presentationData.strings.Notifications_Badge_IncludePublicGroups, inAppSettings.totalUnreadCountIncludeTags.contains(.publicGroups)))
|
||||
entries.append(.includeChannels(presentationData.theme, presentationData.strings.Notifications_Badge_IncludeChannels, inAppSettings.totalUnreadCountIncludeTags.contains(.channels)))
|
||||
|
||||
let counterTagSettings = CounterTagSettings(summaryTags: inAppSettings.totalUnreadCountIncludeTags)
|
||||
|
||||
entries.append(.includePublicGroups(presentationData.theme, presentationData.strings.Notifications_Badge_IncludePublicGroups, counterTagSettings.contains(.publicGroups)))
|
||||
entries.append(.includeChannels(presentationData.theme, presentationData.strings.Notifications_Badge_IncludeChannels, counterTagSettings.contains(.channels)))
|
||||
entries.append(.unreadCountCategory(presentationData.theme, presentationData.strings.Notifications_Badge_CountUnreadMessages, inAppSettings.totalUnreadCountDisplayCategory == .messages))
|
||||
entries.append(.unreadCountCategoryInfo(presentationData.theme, inAppSettings.totalUnreadCountDisplayCategory == .chats ? presentationData.strings.Notifications_Badge_CountUnreadMessages_InfoOff : presentationData.strings.Notifications_Badge_CountUnreadMessages_InfoOn))
|
||||
entries.append(.joinedNotifications(presentationData.theme, presentationData.strings.NotificationSettings_ContactJoined, globalSettings.contactsJoined))
|
||||
@ -911,12 +957,14 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
|
||||
}).start()
|
||||
}, updateIncludeTag: { tag, value in
|
||||
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
|
||||
var settings = settings
|
||||
var currentSettings = CounterTagSettings(summaryTags: settings.totalUnreadCountIncludeTags)
|
||||
if !value {
|
||||
settings.totalUnreadCountIncludeTags.remove(tag)
|
||||
currentSettings.remove(tag)
|
||||
} else {
|
||||
settings.totalUnreadCountIncludeTags.insert(tag)
|
||||
currentSettings.insert(tag)
|
||||
}
|
||||
var settings = settings
|
||||
settings.totalUnreadCountIncludeTags = currentSettings.toSumaryTags()
|
||||
return settings
|
||||
}).start()
|
||||
}, updateTotalUnreadCountCategory: { value in
|
||||
|
@ -139,10 +139,44 @@ public struct OperationLogTags {
|
||||
public static let SynchronizeEmojiKeywords = PeerOperationLogTag(value: 19)
|
||||
}
|
||||
|
||||
public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable {
|
||||
public var rawValue: Int32
|
||||
|
||||
public init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let regularChatsAndPrivateGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 0)
|
||||
public static let publicGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 1)
|
||||
public static let channels = LegacyPeerSummaryCounterTags(rawValue: 1 << 2)
|
||||
|
||||
public func makeIterator() -> AnyIterator<LegacyPeerSummaryCounterTags> {
|
||||
var index = 0
|
||||
return AnyIterator { () -> LegacyPeerSummaryCounterTags? in
|
||||
while index < 31 {
|
||||
let currentTags = self.rawValue >> UInt32(index)
|
||||
let tag = LegacyPeerSummaryCounterTags(rawValue: 1 << UInt32(index))
|
||||
index += 1
|
||||
if currentTags == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if (currentTags & 1) != 0 {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension PeerSummaryCounterTags {
|
||||
static let regularChatsAndPrivateGroups = PeerSummaryCounterTags(rawValue: 1 << 0)
|
||||
static let publicGroups = PeerSummaryCounterTags(rawValue: 1 << 1)
|
||||
static let channels = PeerSummaryCounterTags(rawValue: 1 << 2)
|
||||
static let privateChat = PeerSummaryCounterTags(rawValue: 1 << 3)
|
||||
static let secretChat = PeerSummaryCounterTags(rawValue: 1 << 4)
|
||||
static let privateGroup = PeerSummaryCounterTags(rawValue: 1 << 5)
|
||||
static let bot = PeerSummaryCounterTags(rawValue: 1 << 6)
|
||||
static let channel = PeerSummaryCounterTags(rawValue: 1 << 7)
|
||||
static let publicGroup = PeerSummaryCounterTags(rawValue: 1 << 8)
|
||||
}
|
||||
|
||||
private enum PreferencesKeyValues: Int32 {
|
||||
|
@ -19,19 +19,30 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
|
||||
}
|
||||
|
||||
return SeedConfiguration(globalMessageIdsPeerIdNamespaces: globalMessageIdsPeerIdNamespaces, initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1))), messageHoles: messageHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: MessageTags.unseenPersonalMessage, existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer in
|
||||
if let peer = peer as? TelegramChannel {
|
||||
switch peer.info {
|
||||
case .group:
|
||||
if let addressName = peer.username, !addressName.isEmpty {
|
||||
return [.publicGroups]
|
||||
} else {
|
||||
return [.regularChatsAndPrivateGroups]
|
||||
}
|
||||
case .broadcast:
|
||||
return [.channels]
|
||||
if let peer = peer as? TelegramUser {
|
||||
if peer.botInfo != nil {
|
||||
return .bot
|
||||
} else {
|
||||
return .privateChat
|
||||
}
|
||||
} else if let _ = peer as? TelegramGroup {
|
||||
return .privateGroup
|
||||
} else if let _ = peer as? TelegramSecretChat {
|
||||
return .secretChat
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
switch channel.info {
|
||||
case .broadcast:
|
||||
return .channel
|
||||
case .group:
|
||||
if channel.username != nil {
|
||||
return .publicGroup
|
||||
} else {
|
||||
return .privateGroup
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return [.regularChatsAndPrivateGroups]
|
||||
assertionFailure()
|
||||
return .privateChat
|
||||
}
|
||||
}, additionalChatListIndexNamespace: Namespaces.Message.Cloud, messageNamespacesRequiringGroupStatsValidation: [Namespaces.Message.Cloud], defaultMessageNamespaceReadStates: [Namespaces.Message.Local: .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false)], chatMessagesNamespaces: Set([Namespaces.Message.Cloud, Namespaces.Message.Local, Namespaces.Message.SecretIncoming]))
|
||||
}()
|
||||
|
@ -1330,7 +1330,7 @@ public final class AccountViewTracker {
|
||||
})
|
||||
}
|
||||
|
||||
public func tailChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)? = nil, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> {
|
||||
public func tailChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? = nil, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> {
|
||||
if let account = self.account {
|
||||
return self.wrappedChatListView(signal: account.postbox.tailChatListView(groupId: groupId, filterPredicate: filterPredicate, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud))))
|
||||
} else {
|
||||
@ -1338,7 +1338,7 @@ public final class AccountViewTracker {
|
||||
}
|
||||
}
|
||||
|
||||
public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?) -> Bool)? = nil, index: ChatListIndex, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> {
|
||||
public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? = nil, index: ChatListIndex, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> {
|
||||
if let account = self.account {
|
||||
return self.wrappedChatListView(signal: account.postbox.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: index, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud))))
|
||||
} else {
|
||||
|
@ -54,6 +54,7 @@ private var telegramUIDeclaredEncodables: Void = {
|
||||
declareEncodable(WebBrowserSettings.self, f: { WebBrowserSettings(decoder: $0) })
|
||||
declareEncodable(IntentsSettings.self, f: { IntentsSettings(decoder: $0) })
|
||||
declareEncodable(CachedGeocode.self, f: { CachedGeocode(decoder: $0) })
|
||||
declareEncodable(ChatListFilterSettings.self, f: { ChatListFilterSettings(decoder: $0) })
|
||||
return
|
||||
}()
|
||||
|
||||
|
@ -0,0 +1,127 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import SyncCore
|
||||
|
||||
public struct ChatListIncludeCategoryFilter: OptionSet {
|
||||
public var rawValue: Int32
|
||||
|
||||
public init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let muted = ChatListIncludeCategoryFilter(rawValue: 1 << 1)
|
||||
public static let privateChats = ChatListIncludeCategoryFilter(rawValue: 1 << 2)
|
||||
public static let secretChats = ChatListIncludeCategoryFilter(rawValue: 1 << 3)
|
||||
public static let privateGroups = ChatListIncludeCategoryFilter(rawValue: 1 << 4)
|
||||
public static let bots = ChatListIncludeCategoryFilter(rawValue: 1 << 5)
|
||||
public static let publicGroups = ChatListIncludeCategoryFilter(rawValue: 1 << 6)
|
||||
public static let channels = ChatListIncludeCategoryFilter(rawValue: 1 << 7)
|
||||
public static let read = ChatListIncludeCategoryFilter(rawValue: 1 << 8)
|
||||
|
||||
public static let all: ChatListIncludeCategoryFilter = [
|
||||
.muted,
|
||||
.privateChats,
|
||||
.secretChats,
|
||||
.privateGroups,
|
||||
.bots,
|
||||
.publicGroups,
|
||||
.channels,
|
||||
.read
|
||||
]
|
||||
}
|
||||
|
||||
public enum ChatListFilterPresetName: Equatable, Hashable, PostboxCoding {
|
||||
case unread
|
||||
case custom(String)
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
switch decoder.decodeInt32ForKey("_t", orElse: 0) {
|
||||
case 0:
|
||||
self = .unread
|
||||
case 1:
|
||||
self = .custom(decoder.decodeStringForKey("title", orElse: "Preset"))
|
||||
default:
|
||||
assertionFailure()
|
||||
self = .custom("Preset")
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
switch self {
|
||||
case .unread:
|
||||
encoder.encodeInt32(0, forKey: "_t")
|
||||
case let .custom(title):
|
||||
encoder.encodeInt32(1, forKey: "_t")
|
||||
encoder.encodeString(title, forKey: "title")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatListFilterPreset: Equatable, PostboxCoding {
|
||||
public var name: ChatListFilterPresetName
|
||||
public var includeCategories: ChatListIncludeCategoryFilter
|
||||
public var additionallyIncludePeers: [PeerId]
|
||||
|
||||
public init(name: ChatListFilterPresetName, includeCategories: ChatListIncludeCategoryFilter, additionallyIncludePeers: [PeerId]) {
|
||||
self.name = name
|
||||
self.includeCategories = includeCategories
|
||||
self.additionallyIncludePeers = additionallyIncludePeers
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.name = decoder.decodeObjectForKey("name", decoder: { ChatListFilterPresetName(decoder: $0) }) as? ChatListFilterPresetName ?? ChatListFilterPresetName.custom("Preset")
|
||||
self.includeCategories = ChatListIncludeCategoryFilter(rawValue: decoder.decodeInt32ForKey("includeCategories", orElse: 0))
|
||||
self.additionallyIncludePeers = decoder.decodeInt64ArrayForKey("additionallyIncludePeers").map(PeerId.init)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(self.name, forKey: "name")
|
||||
encoder.encodeInt32(self.includeCategories.rawValue, forKey: "includeCategories")
|
||||
encoder.encodeInt64Array(self.additionallyIncludePeers.map { $0.toInt64() }, forKey: "additionallyIncludePeers")
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatListFilterSettings: PreferencesEntry, Equatable {
|
||||
public var presets: [ChatListFilterPreset]
|
||||
|
||||
public static var `default`: ChatListFilterSettings {
|
||||
return ChatListFilterSettings(presets: [
|
||||
ChatListFilterPreset(
|
||||
name: .unread,
|
||||
includeCategories: ChatListIncludeCategoryFilter.all.subtracting(.read),
|
||||
additionallyIncludePeers: []
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
public init(presets: [ChatListFilterPreset]) {
|
||||
self.presets = presets
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.presets = decoder.decodeObjectArrayWithDecoderForKey("presets")
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObjectArray(self.presets, forKey: "presets")
|
||||
}
|
||||
|
||||
public func isEqual(to: PreferencesEntry) -> Bool {
|
||||
if let to = to as? ChatListFilterSettings {
|
||||
return self == to
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateChatListFilterSettingsInteractively(postbox: Postbox, _ f: @escaping (ChatListFilterSettings) -> ChatListFilterSettings) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Void in
|
||||
transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.chatListFilterSettings, { entry in
|
||||
var settings = entry as? ChatListFilterSettings ?? ChatListFilterSettings.default
|
||||
return f(settings)
|
||||
})
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import SyncCore
|
||||
|
||||
public enum TotalUnreadCountDisplayStyle: Int32 {
|
||||
case filtered = 0
|
||||
@ -38,7 +39,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
||||
public var displayNotificationsFromAllAccounts: Bool
|
||||
|
||||
public static var defaultSettings: InAppNotificationSettings {
|
||||
return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: [.regularChatsAndPrivateGroups], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true)
|
||||
return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: [.privateChat, .secretChat, .bot, .privateGroup], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true)
|
||||
}
|
||||
|
||||
public init(playSounds: Bool, vibrate: Bool, displayPreviews: Bool, totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: PeerSummaryCounterTags, displayNameOnLockscreen: Bool, displayNotificationsFromAllAccounts: Bool) {
|
||||
@ -58,10 +59,25 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
||||
self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
||||
self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("cds", orElse: 0)) ?? .filtered
|
||||
self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 1)) ?? .messages
|
||||
if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") {
|
||||
if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags_2") {
|
||||
self.totalUnreadCountIncludeTags = PeerSummaryCounterTags(rawValue: value)
|
||||
} else if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") {
|
||||
var resultTags: PeerSummaryCounterTags = []
|
||||
for legacyTag in LegacyPeerSummaryCounterTags(rawValue: value) {
|
||||
if legacyTag == .regularChatsAndPrivateGroups {
|
||||
resultTags.insert(.privateChat)
|
||||
resultTags.insert(.secretChat)
|
||||
resultTags.insert(.bot)
|
||||
resultTags.insert(.privateGroup)
|
||||
} else if legacyTag == .publicGroups {
|
||||
resultTags.insert(.publicGroup)
|
||||
} else if legacyTag == .channels {
|
||||
resultTags.insert(.channel)
|
||||
}
|
||||
}
|
||||
self.totalUnreadCountIncludeTags = resultTags
|
||||
} else {
|
||||
self.totalUnreadCountIncludeTags = [.regularChatsAndPrivateGroups]
|
||||
self.totalUnreadCountIncludeTags = [.privateChat, .secretChat, .bot, .privateGroup]
|
||||
}
|
||||
self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0
|
||||
self.displayNotificationsFromAllAccounts = decoder.decodeInt32ForKey("displayNotificationsFromAllAccounts", orElse: 1) != 0
|
||||
@ -73,7 +89,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
||||
encoder.encodeInt32(self.displayPreviews ? 1 : 0, forKey: "p")
|
||||
encoder.encodeInt32(self.totalUnreadCountDisplayStyle.rawValue, forKey: "cds")
|
||||
encoder.encodeInt32(self.totalUnreadCountDisplayCategory.rawValue, forKey: "totalUnreadCountDisplayCategory")
|
||||
encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags")
|
||||
encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags_2")
|
||||
encoder.encodeInt32(self.displayNameOnLockscreen ? 1 : 0, forKey: "displayNameOnLockscreen")
|
||||
encoder.encodeInt32(self.displayNotificationsFromAllAccounts ? 1 : 0, forKey: "displayNotificationsFromAllAccounts")
|
||||
}
|
||||
|
@ -6,11 +6,13 @@ import Postbox
|
||||
private enum ApplicationSpecificPreferencesKeyValues: Int32 {
|
||||
case voipDerivedState = 16
|
||||
case chatArchiveSettings = 17
|
||||
case chatListFilterSettings = 18
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificPreferencesKeys {
|
||||
public static let voipDerivedState = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voipDerivedState.rawValue)
|
||||
public static let chatArchiveSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatArchiveSettings.rawValue)
|
||||
public static let chatListFilterSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatListFilterSettings.rawValue)
|
||||
}
|
||||
|
||||
private enum ApplicationSpecificSharedDataKeyValues: Int32 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user