Experimental chat list filtering

This commit is contained in:
Ali 2020-01-28 16:29:00 +04:00
parent 5112310a32
commit 309a8b112b
22 changed files with 1752 additions and 140 deletions

View File

@ -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")
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View 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
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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]))
}()

View File

@ -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 {

View File

@ -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
}()

View File

@ -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
}

View File

@ -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")
}

View File

@ -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 {