Update filters

This commit is contained in:
Ali 2020-03-04 02:10:49 +04:00
parent d14e0772e7
commit 4397baa226
87 changed files with 3840 additions and 1931 deletions

View file

@ -1,3 +1,5 @@
build --experimental_guard_against_concurrent_changes
build --strategy=Genrule=local
build --apple_platform_type=ios
build --cxxopt='-std=c++14'

View file

@ -8,6 +8,14 @@ if [ "$BAZEL" = "" ]; then
exit 1
fi
XCODE_VERSION=$(cat "build-system/xcode_version")
INSTALLED_XCODE_VERSION=$(echo `plutil -p \`xcode-select -p\`/../Info.plist | grep -e CFBundleShortVersionString | sed 's/[^0-9\.]*//g'`)
if [ "$INSTALLED_XCODE_VERSION" != "$XCODE_VERSION" ]; then
echo "Xcode $XCODE_VERSION required, $INSTALLED_XCODE_VERSION installed (at $(xcode-select -p))"
exit 1
fi
EXPECTED_VARIABLES=(\
BUILD_NUMBER \
APP_VERSION \
@ -37,7 +45,7 @@ rm -rf "$GEN_DIRECTORY"
mkdir -p "$GEN_DIRECTORY"
pushd "build-system/tulsi"
"$BAZEL" build //:tulsi --xcode_version=$(cat "build-system/xcode_version")
"$BAZEL" build //:tulsi --xcode_version="$XCODE_VERSION"
popd
TULSI_DIRECTORY="build-input/gen/project"

View file

@ -1,13 +1,36 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
public struct ChatListNodeAdditionalCategory {
public var id: Int
public var icon: UIImage?
public var title: String
public init(id: Int, icon: UIImage?, title: String) {
self.id = id
self.icon = icon
self.title = title
}
}
public struct ContactMultiselectionControllerAdditionalCategories {
public var categories: [ChatListNodeAdditionalCategory]
public var selectedCategories: Set<Int>
public init(categories: [ChatListNodeAdditionalCategory], selectedCategories: Set<Int>) {
self.categories = categories
self.selectedCategories = selectedCategories
}
}
public enum ContactMultiselectionControllerMode {
case groupCreation
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
case channelCreation
case chatSelection
case chatSelection(selectedChats: Set<PeerId>, additionalCategories: ContactMultiselectionControllerAdditionalCategories?)
}
public enum ContactListFilter {
@ -21,17 +44,24 @@ public final class ContactMultiselectionControllerParams {
public let mode: ContactMultiselectionControllerMode
public let options: [ContactListAdditionalOption]
public let filters: [ContactListFilter]
public let alwaysEnabled: Bool
public init(context: AccountContext, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter] = [.excludeSelf]) {
public init(context: AccountContext, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter] = [.excludeSelf], alwaysEnabled: Bool = false) {
self.context = context
self.mode = mode
self.options = options
self.filters = filters
self.alwaysEnabled = alwaysEnabled
}
}
public enum ContactMultiselectionResult {
case none
case result(peerIds: [ContactListPeerId], additionalOptionIds: [Int])
}
public protocol ContactMultiselectionController: ViewController {
var result: Signal<[ContactListPeerId], NoError> { get }
var result: Signal<ContactMultiselectionResult, NoError> { get }
var displayProgress: Bool { get set }
var dismissed: (() -> Void)? { get set }
}

View file

@ -274,9 +274,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
self.height = height
self.bytesPerRow = (4 * Int(width) + 15) & (~15)
self.currentFrame = 0
guard let rawData = TGGUnzipData(data, 8 * 1024 * 1024) else {
return nil
}
let rawData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
guard let animation = LottieInstance(data: rawData, cacheKey: "") else {
return nil
}

View file

@ -427,7 +427,7 @@ public final class AvatarNode: ASDisplayNode {
if let explicitColorIndex = parameters.explicitColorIndex {
colorIndex = explicitColorIndex
} else {
if let accountPeerId = parameters.accountPeerId, let peerId = parameters.peerId {
if let peerId = parameters.peerId {
if peerId.namespace == -1 {
colorIndex = -1
} else {
@ -622,3 +622,65 @@ public func drawPeerAvatarLetters(context: CGContext, size: CGSize, font: UIFont
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
context.textPosition = textPosition
}
public enum AvatarBackgroundColor {
case blue
case yellow
case green
case purple
case red
case violet
}
public func generateAvatarImage(size: CGSize, icon: UIImage?, color: AvatarBackgroundColor) -> UIImage? {
return generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.beginPath()
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height:
size.height))
context.clip()
let colorIndex: Int
switch color {
case .blue:
colorIndex = 5
case .yellow:
colorIndex = 1
case .green:
colorIndex = 3
case .purple:
colorIndex = 2
case .red:
colorIndex = 0
case .violet:
colorIndex = 6
}
let colorsArray: NSArray
if colorIndex == -1 {
colorsArray = grayscaleColors
} else {
colorsArray = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count]
}
var locations: [CGFloat] = [1.0, 0.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
context.resetClip()
context.setBlendMode(.normal)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
if let icon = icon {
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - icon.size.width) / 2.0), y: floor((size.height - icon.size.height) / 2.0)), size: icon.size)
context.draw(icon.cgImage!, in: iconFrame)
}
})
}

View file

@ -19,6 +19,8 @@ public enum ChatListSearchItemHeaderType: Int32 {
case addToExceptions
case mapAddress
case nearbyVenues
case chats
case chatTypes
}
public final class ChatListSearchItemHeader: ListViewItemHeader {
@ -101,6 +103,11 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode {
self.sectionHeaderNode.title = strings.Map_AddressOnMap.uppercased()
case .nearbyVenues:
self.sectionHeaderNode.title = strings.Map_PlacesNearby.uppercased()
case .chats:
self.sectionHeaderNode.title = strings.Cache_ByPeerHeader.uppercased()
case .chatTypes:
//TODO:localize
self.sectionHeaderNode.title = "CHAT TYPES"
}
self.sectionHeaderNode.action = actionTitle
@ -147,6 +154,11 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode {
self.sectionHeaderNode.title = strings.Map_AddressOnMap.uppercased()
case .nearbyVenues:
self.sectionHeaderNode.title = strings.Map_PlacesNearby.uppercased()
case .chats:
self.sectionHeaderNode.title = strings.Cache_ByPeerHeader.uppercased()
case .chatTypes:
//TODO:localize
self.sectionHeaderNode.title = "CHAT TYPES"
}
self.sectionHeaderNode.action = actionTitle

View file

@ -45,6 +45,7 @@ static_library(
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/TooltipUI:TooltipUI",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View file

@ -44,6 +44,8 @@ swift_library(
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/TelegramIntents:TelegramIntents",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TooltipUI:TooltipUI",
],
visibility = [
"//visibility:public",

View file

@ -0,0 +1,327 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import ItemListUI
import CheckNode
import AvatarNode
import AccountContext
import TelegramPresentationData
import ChatListSearchItemHeader
public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeader {
let presentationData: ItemListPresentationData
public let sectionId: ItemListSectionId
let context: AccountContext
let title: String
let image: UIImage?
let isSelected: Bool
let action: () -> Void
public let selectable: Bool = true
public let header: ListViewItemHeader?
public init(
presentationData: ItemListPresentationData,
sectionId: ItemListSectionId = 0,
context: AccountContext,
title: String,
image: UIImage?,
isSelected: Bool,
action: @escaping () -> Void
) {
self.presentationData = presentationData
self.sectionId = sectionId
self.context = context
self.title = title
self.image = image
self.isSelected = isSelected
self.action = action
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
public 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 = ChatListAdditionalCategoryItemNode()
let makeLayout = node.asyncLayout()
let (first, last, firstWithHeader) = ChatListAdditionalCategoryItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
Queue.mainQueue().async {
completion(node, {
let (signal, apply) = nodeApply()
return (signal, { _ in
apply(false, synchronousLoads)
})
})
}
}
}
public 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? ChatListAdditionalCategoryItemNode {
let layout = nodeValue.asyncLayout()
async {
let (first, last, firstWithHeader) = ChatListAdditionalCategoryItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply().1(animation.isAnimated, false)
})
}
}
}
}
}
public func selected(listView: ListView) {
self.action()
}
static func mergeType(item: ChatListAdditionalCategoryItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) {
var first = false
var last = false
var firstWithHeader = false
if let previousItem = previousItem {
if let header = item.header {
if let previousItem = previousItem as? ListViewItemWithHeader {
firstWithHeader = header.id != previousItem.header?.id
} else {
firstWithHeader = true
}
}
} else {
first = true
firstWithHeader = item.header != nil
}
if let nextItem = nextItem {
if let header = item.header {
if let nextItem = nextItem as? ListViewItemWithHeader {
last = header.id != nextItem.header?.id
} else {
last = true
}
}
} else {
last = true
}
return (first, last, firstWithHeader)
}
}
private let avatarFont = avatarPlaceholderFont(size: 16.0)
public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topSeparatorNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let avatarNode: ASImageNode
private let titleNode: TextNode
private var selectionNode: CheckNode?
private var isHighlighted: Bool = false
private var item: ChatListAdditionalCategoryItem?
required public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.avatarNode = ASImageNode()
self.titleNode = TextNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.isAccessibilityElement = true
self.addSubnode(self.backgroundNode)
self.addSubnode(self.topSeparatorNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.titleNode)
}
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let (first, last, firstWithHeader) = ChatListAdditionalCategoryItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem)
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader, itemListNeighbors(item: item, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
return
/*super.setHighlighted(highlighted, at: point, animated: animated)
self.isHighlighted = highlighted
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)*/
}
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
}
public func asyncLayout() -> (_ item: ChatListAdditionalCategoryItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (Bool, Bool) -> Void)) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentSelectionNode = self.selectionNode
let currentItem = self.item
return { [weak self] item, params, first, last, firstWithHeader, neighbors in
var updatedTheme: PresentationTheme?
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let avatarDiameter: CGFloat = 40.0
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let leftInset: CGFloat = 65.0 + params.leftInset
var rightInset: CGFloat = 10.0 + params.rightInset
let updatedSelectionNode: CheckNode?
let isSelected = item.isSelected
rightInset += 28.0
let selectionNode: CheckNode
if let current = currentSelectionNode {
selectionNode = current
updatedSelectionNode = selectionNode
} else {
selectionNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain)
selectionNode.isUserInteractionEnabled = false
updatedSelectionNode = selectionNode
}
var titleAttributedString: NSAttributedString?
let textColor = item.presentationData.theme.list.itemPrimaryTextColor
titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: textColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat = 13.0
let statusHeightComponent: CGFloat
statusHeightComponent = 0.0
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + statusHeightComponent), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
let titleFrame: CGRect
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
return (nodeLayout, { [weak self] in
if let strongSelf = self {
return (.complete(), { [weak strongSelf] animated, synchronousLoads in
if let strongSelf = strongSelf {
strongSelf.item = item
strongSelf.accessibilityLabel = titleAttributedString?.string
//strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
//strongSelf.containerNode.isGestureEnabled = item.contextAction != nil
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
let revealOffset = strongSelf.revealOffset
if let _ = updatedTheme {
strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
strongSelf.avatarNode.image = item.image
strongSelf.topSeparatorNode.isHidden = true
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)))
let _ = titleApply()
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0))
if let updatedSelectionNode = updatedSelectionNode {
if strongSelf.selectionNode !== updatedSelectionNode {
strongSelf.selectionNode?.removeFromSupernode()
strongSelf.selectionNode = updatedSelectionNode
strongSelf.addSubnode(updatedSelectionNode)
}
updatedSelectionNode.setIsChecked(isSelected, animated: animated)
updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 32.0 - 12.0, y: floor((nodeLayout.contentSize.height - 32.0) / 2.0)), size: CGSize(width: 32.0, height: 32.0))
} else if let selectionNode = strongSelf.selectionNode {
selectionNode.removeFromSupernode()
strongSelf.selectionNode = nil
}
let separatorHeight = UIScreenPixel
let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset))
strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(nodeLayout.insets.top, separatorHeight)), size: CGSize(width: nodeLayout.contentSize.width, height: separatorHeight))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - leftInset), height: separatorHeight))
strongSelf.separatorNode.isHidden = last
strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
}
})
} else {
return (nil, { _, _ in
})
}
})
}
}
override public func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
let bounds = self.bounds
accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0))
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
override public func header() -> ListViewItemHeader? {
if let item = self.item {
return item.header
} else {
return nil
}
}
}

View file

@ -22,28 +22,20 @@ import ContextUI
import AppBundle
import LocalizedPeerData
import TelegramIntents
import TooltipUI
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if listNode.scroller.isDragging {
return false
}
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
let scrollToItem: ListViewScrollToItem
let targetProgress: CGFloat
let offset: CGFloat
if searchNode.expansionProgress < 0.6 {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: true, curve: .Default(duration: nil), directionHint: .Up)
targetProgress = 0.0
offset = navigationBarSearchContentHeight
} else {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up)
targetProgress = 1.0
offset = 0.0
}
//searchNode.updateExpansionProgress(targetProgress, animated: true)
//listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
listNode.scrollToOffsetFromTop(offset)
let _ = listNode.scrollToOffsetFromTop(offset)
return true
} else if searchNode.expansionProgress == 1.0 {
var sortItemNode: ListViewItemNode?
@ -178,7 +170,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
let title: String
if let filter = self.filter {
title = filter.title ?? ""
title = filter.title
} else if self.groupId == .root {
title = self.presentationData.strings.DialogList_Title
self.navigationBar?.item = nil
@ -406,7 +398,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
if self.filter == nil {
self.chatListDisplayNode.containerNode.currentItemFilterUpdated = { [weak self] filter, fraction, transition in
self.chatListDisplayNode.containerNode.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in
guard let strongSelf = self else {
return
}
@ -416,6 +408,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let tabContainerData = strongSelf.tabContainerData else {
return
}
if force {
strongSelf.tabContainerNode.cancelAnimations()
}
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData, selectedFilter: filter, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition)
}
let preferencesKey: PostboxViewKey = .preferences(keys: Set([
@ -428,26 +423,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
]),
filterItems
)
|> deliverOnMainQueue).start(next: { [weak self] combinedView, countAndFilterItems in
|> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems in
guard let strongSelf = self else {
return
}
let (totalCount, items) = countAndFilterItems
let (_, items) = countAndFilterItems
var filterItems: [ChatListFilterTabEntry] = []
filterItems.append(.all(unreadCount: 0))
for (filter, unreadCount) in items {
filterItems.append(.filter(id: filter.id, text: filter.title, unreadCount: unreadCount))
}
var filterSettings: ChatListFilterSettings = .default
if let preferencesView = combinedView.views[preferencesKey] as? PreferencesView {
if let value = preferencesView.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings] as? ChatListFilterSettings {
filterSettings = value
}
}
var resolvedItems = filterItems
if !filterSettings.displayTabs || groupId != .root {
if groupId != .root {
resolvedItems = []
}
@ -498,7 +486,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
guard let strongSelf = self else {
return
}
let previousFilter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter
let updatedFilter: ChatListFilter?
switch id {
case .all:
@ -564,6 +551,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
break
}
}
if !found {
f(.default)
}
})
})
})))
@ -593,10 +583,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
break
}
}
if !found {
f(.default)
}
})
})
})))
items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Remove", textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { c, f in
c.dismiss(completion: {
@ -607,6 +601,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: "This will remove the filter, your chats will not be deleted."),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
@ -690,6 +685,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
if let layout = self.validLayout {
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate)
}
if self.isNodeLoaded {
self.chatListDisplayNode.updatePresentationData(self.presentationData)
}
@ -716,7 +715,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
self.chatListDisplayNode.containerNode.present = { [weak self] c in
if let strongSelf = self {
self?.present(c, in: .window(.root))
strongSelf.present(c, in: .window(.root))
}
}
@ -905,14 +904,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
}
self.chatListDisplayNode.isEmptyUpdated = { [weak self] isEmpty in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode, let _ = strongSelf.validLayout {
if isEmpty {
//searchContentNode.updateListVisibleContentOffset(.known(0.0))
}
}
}
self.chatListDisplayNode.emptyListAction = { [weak self] in
guard let strongSelf = self else {
return
@ -1123,6 +1114,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
_ = markSuggestedLocalizationAsSeenInteractively(postbox: strongSelf.context.account.postbox, languageCode: suggestedLocalization.languageCode).start()
}
}))
Queue.mainQueue().after(1.0, {
if let _ = self.validLayout, let parentController = self.parent as? TabBarController, let sourceFrame = parentController.frameForControllerTab(controller: self) {
let absoluteFrame = sourceFrame
//TODO:localize
parentController.present(TooltipScreen(text: "Hold the Chats icon for quick access to the list of chat filters.", location: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0)), in: .current)
}
})
}
self.chatListDisplayNode.containerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in
@ -1178,7 +1177,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
var tabContainerOffset: CGFloat = 0.0
if !self.displayNavigationBar {
tabContainerOffset += layout.statusBarHeight ?? 0.0
tabContainerOffset += 44.0 + 44.0 + 44.0
tabContainerOffset += 44.0 + 20.0
}
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.visualNavigationInsetHeight - self.additionalHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0)))
@ -1443,8 +1442,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
let peerIds = self.chatListDisplayNode.containerNode.currentItemNode.currentState.selectedPeerIds
if case .left = action {
let signal: Signal<Void, NoError>
var completion: (() -> Void)?
let context = self.context
if !peerIds.isEmpty {
self.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!)
completion = { [weak self] in
self?.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(nil)
}
signal = self.context.account.postbox.transaction { transaction -> Void in
for peerId in peerIds {
togglePeerUnreadMarkInteractively(transaction: transaction, viewTracker: context.account.viewTracker, peerId: peerId, setToValue: false)
@ -1459,6 +1463,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
let _ = (signal
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.donePressed()
completion?()
})
} else if case .right = action, !peerIds.isEmpty {
let actionSheet = ActionSheetController(presentationData: self.presentationData)
@ -2134,8 +2139,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
case channels
case groups
case bots
case secretChats
case privateChats
case contacts
case nonContacts
}
let filterType: ChatListFilterType
if preset.data.includePeers.isEmpty {
@ -2150,14 +2155,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} else {
if preset.data.categories == .channels {
filterType = .channels
} else if preset.data.categories.isSubset(of: [.publicGroups, .privateGroups]) {
} else if preset.data.categories.isSubset(of: [.smallGroups, .largeGroups]) {
filterType = .groups
} else if preset.data.categories == .bots {
filterType = .bots
} else if preset.data.categories == .secretChats {
filterType = .secretChats
} else if preset.data.categories == .privateChats {
filterType = .privateChats
} else if preset.data.categories == .contacts {
filterType = .contacts
} else if preset.data.categories == .nonContacts {
filterType = .nonContacts
} else {
filterType = .generic
}
@ -2171,7 +2176,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
badge = "\(item.1)"
}
}
items.append(.action(ContextMenuActionItem(text: preset.title ?? "", badge: badge, icon: { theme in
items.append(.action(ContextMenuActionItem(text: preset.title, badge: badge, icon: { theme in
let imageName: String
switch filterType {
case .generic:
@ -2186,10 +2191,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
imageName = "Chat/Context Menu/Groups"
case .bots:
imageName = "Chat/Context Menu/Bots"
case .secretChats:
imageName = "Chat/Context Menu/Timer"
case .privateChats:
case .contacts:
imageName = "Chat/Context Menu/User"
case .nonContacts:
imageName = "Chat/Context Menu/UnknownUser"
}
return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor)
}, action: { _, f in
@ -2208,7 +2213,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
}
override public func tabBarItemSwipeAction(direction: TabBarItemSwipeDirection) {
guard let entries = self.tabContainerData, var index = entries.index(where: { $0.id == self.chatListDisplayNode.containerNode.currentItemFilter }) else {
guard let entries = self.tabContainerData, var index = entries.firstIndex(where: { $0.id == self.chatListDisplayNode.containerNode.currentItemFilter }) else {
return
}
switch direction {

View file

@ -70,7 +70,7 @@ private final class ChatListContainerItemNode: ASDisplayNode {
self.addSubnode(self.listNode)
self.listNode.isEmptyUpdated = { [weak self] isEmptyState, _, _, transition in
self.listNode.isEmptyUpdated = { [weak self] isEmptyState, _, transition in
guard let strongSelf = self else {
return
}
@ -113,11 +113,12 @@ private final class ChatListContainerItemNode: ASDisplayNode {
func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, visualNavigationHeight)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: 0.0, curve: .Default(duration: 0.0))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
self.listNode.visualInsets = UIEdgeInsets(top: visualNavigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
self.listNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets)
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
if let emptyNode = self.emptyNode {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
@ -142,6 +143,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
private var selectedId: ChatListFilterTabEntryId
private(set) var transitionFraction: CGFloat = 0.0
private var transitionFractionOffset: CGFloat = 0.0
private var disableItemNodeOperationsWhileAnimating: Bool = false
private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat)?
@ -160,7 +162,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
return self.currentItemStateValue.get()
}
var currentItemFilterUpdated: ((ChatListFilterTabEntryId, CGFloat, ContainedViewLayoutTransition) -> Void)?
var currentItemFilterUpdated: ((ChatListFilterTabEntryId, CGFloat, ContainedViewLayoutTransition, Bool) -> Void)?
var currentItemFilter: ChatListFilterTabEntryId {
return self.currentItemNode.chatListFilter.flatMap { .filter($0.id) } ?? .all
}
@ -264,7 +266,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
self.applyItemNodeAsCurrent(id: .all, itemNode: itemNode)
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, let index = strongSelf.availableFilters.index(where: { $0.id == strongSelf.selectedId }) else {
guard let strongSelf = self, let index = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }) else {
return []
}
var directions: InteractiveTransitionGestureRecognizerDirections = [.leftCenter, .rightCenter]
@ -279,7 +281,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
directions = []
}
return directions
})
}, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0))
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
@ -306,8 +308,23 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.transitionFractionOffset = 0.0
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let itemNode = self.itemNodes[self.selectedId] {
if let presentationLayer = itemNode.layer.presentation() {
self.transitionFraction = presentationLayer.frame.minX / layout.size.width
self.transitionFractionOffset = self.transitionFraction
if !self.transitionFraction.isZero {
for (_, itemNode) in self.itemNodes {
itemNode.layer.removeAllAnimations()
}
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, true)
}
}
}
case .changed:
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.index(where: { $0.id == self.selectedId }) {
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
let translation = recognizer.translation(in: self.view)
var transitionFraction = translation.x / layout.size.width
if selectedIndex <= 0 {
@ -316,7 +333,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
if selectedIndex >= self.availableFilters.count - 1 {
transitionFraction = max(0.0, transitionFraction)
}
self.transitionFraction = transitionFraction
self.transitionFraction = transitionFraction + self.transitionFractionOffset
if let currentItemNode = self.currentItemNodeValue {
let isNavigationHidden = currentItemNode.listNode.isNavigationHidden
for (_, itemNode) in self.itemNodes {
@ -326,15 +343,27 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, false)
}
case .cancelled, .ended:
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.index(where: { $0.id == self.selectedId }) {
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
let translation = recognizer.translation(in: self.view)
let velocity = recognizer.velocity(in: self.view)
var directionIsToRight: Bool?
if abs(velocity.x) > 10.0 {
directionIsToRight = velocity.x < 0.0
if translation.x < 0.0 {
if velocity.x >= 0.0 {
directionIsToRight = nil
} else {
directionIsToRight = true
}
} else {
if velocity.x <= 0.0 {
directionIsToRight = nil
} else {
directionIsToRight = false
}
}
} else {
if abs(translation.x) > layout.size.width / 2.0 {
directionIsToRight = translation.x > layout.size.width / 2.0
@ -357,16 +386,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
self.disableItemNodeOperationsWhileAnimating = true
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition)
/*transition.updateBounds(node: self, bounds: self.bounds, force: true, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.disableItemNodeOperationsWhileAnimating = false
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = strongSelf.validLayout {
strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
}
})*/
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false)
DispatchQueue.main.async {
self.disableItemNodeOperationsWhileAnimating = false
if let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout {
@ -437,7 +457,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight) = self.validLayout else {
return
}
if id != self.selectedId, let index = self.availableFilters.index(where: { $0.id == id }) {
if id != self.selectedId, let index = self.availableFilters.firstIndex(where: { $0.id == id }) {
if let itemNode = self.itemNodes[id] {
self.selectedId = id
if let currentItemNode = self.currentItemNodeValue {
@ -446,7 +466,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
self.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: transition)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition)
self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false)
} else if self.pendingItemNode == nil {
let itemNode = ChatListContainerItemNode(context: self.context, groupId: self.groupId, filter: self.availableFilters[index].filter, previewing: self.previewing, presentationData: self.presentationData, becameEmpty: { [weak self] filter in
self?.filterBecameEmpty(filter)
@ -470,7 +490,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
if let previousIndex = strongSelf.availableFilters.index(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.index(where: { $0.id == id }) {
if let previousIndex = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.firstIndex(where: { $0.id == id }) {
let previousId = strongSelf.selectedId
let offsetDirection: CGFloat = index < previousIndex ? 1.0 : -1.0
let offset = offsetDirection * layout.size.width
@ -522,7 +542,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, transition)
strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, transition, false)
}
}))
}
@ -538,7 +558,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
if let selectedIndex = self.availableFilters.index(where: { $0.id == self.selectedId }) {
if let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
var validNodeIds: [ChatListFilterTabEntryId] = []
for i in max(0, selectedIndex - 1) ... min(self.availableFilters.count - 1, selectedIndex + 1) {
let id = self.availableFilters[i].id
@ -561,7 +581,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
if !validNodeIds.contains(id) {
removeIds.append(id)
}
guard let index = self.availableFilters.index(where: { $0.id == id }) else {
guard let index = self.availableFilters.firstIndex(where: { $0.id == id }) else {
continue
}
let indexDistance = CGFloat(index - selectedIndex) + self.transitionFraction
@ -577,10 +597,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
if !wasAdded && slidingOffset == nil {
slidingOffset = itemNode.frame.minX - itemFrame.minX
}
nodeTransition.updateFrame(node: itemNode, frame: itemFrame, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
nodeTransition.updateFrame(node: itemNode, frame: itemFrame, completion: { _ in
})
itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, transition: nodeTransition)
@ -760,7 +777,7 @@ final class ChatListControllerNode: ASDisplayNode {
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight, _, cleanNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else {
guard let (containerLayout, _, _, cleanNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else {
return
}

View file

@ -8,138 +8,6 @@ import AppBundle
import SolidRoundedButtonNode
import ActivityIndicator
final class ChatListEmptyNodeContainer: ASDisplayNode {
private var currentNode: ChatListEmptyNode?
private var theme: PresentationTheme
private var strings: PresentationStrings
private var validLayout: CGSize?
var action: (() -> Void)?
init(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
super.init()
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
if let currentNode = self.currentNode {
currentNode.updateThemeAndStrings(theme: theme, strings: strings)
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
if let currentNode = self.currentNode {
currentNode.updateLayout(size: size, transition: transition)
}
}
func update(state: ChatListNodeEmptyState, isFilter: Bool, direction: ChatListNodePaneSwitchAnimationDirection?, transition: ContainedViewLayoutTransition) {
switch state {
case let .empty(isLoading):
if let direction = direction {
let previousNode = self.currentNode
let currentNode = ChatListEmptyNode(isFilter: isFilter, isLoading: isLoading, theme: self.theme, strings: self.strings, action: { [weak self] in
self?.action?()
})
self.currentNode = currentNode
if let size = self.validLayout {
currentNode.frame = CGRect(origin: CGPoint(), size: size)
currentNode.updateLayout(size: size, transition: .immediate)
}
self.addSubnode(currentNode)
if case .animated = transition, let size = self.validLayout {
let offset: CGFloat
switch direction {
case .left:
offset = -size.width
case .right:
offset = size.width
}
if let previousNode = previousNode {
previousNode.frame = self.bounds.offsetBy(dx: offset, dy: 0.0)
}
transition.animateHorizontalOffsetAdditive(node: self, offset: offset, completion: { [weak previousNode] in
previousNode?.removeFromSupernode()
})
} else {
previousNode?.removeFromSupernode()
}
} else {
if let previousNode = self.currentNode, previousNode.isFilter != isFilter {
let currentNode = ChatListEmptyNode(isFilter: isFilter, isLoading: isLoading, theme: self.theme, strings: self.strings, action: { [weak self] in
self?.action?()
})
self.currentNode = currentNode
if let size = self.validLayout {
currentNode.frame = CGRect(origin: CGPoint(), size: size)
currentNode.updateLayout(size: size, transition: .immediate)
}
self.addSubnode(currentNode)
currentNode.alpha = 0.0
transition.updateAlpha(node: currentNode, alpha: 1.0)
transition.updateAlpha(node: previousNode, alpha: 0.0, completion: { [weak previousNode] _ in
previousNode?.removeFromSupernode()
})
} else if let currentNode = self.currentNode {
currentNode.updateIsLoading(isLoading)
} else {
let currentNode = ChatListEmptyNode(isFilter: isFilter, isLoading: isLoading, theme: self.theme, strings: self.strings, action: { [weak self] in
self?.action?()
})
self.currentNode = currentNode
if let size = self.validLayout {
currentNode.frame = CGRect(origin: CGPoint(), size: size)
currentNode.updateLayout(size: size, transition: .immediate)
}
self.addSubnode(currentNode)
currentNode.alpha = 0.0
transition.updateAlpha(node: currentNode, alpha: 1.0)
}
}
case .notEmpty:
if let previousNode = self.currentNode {
self.currentNode = nil
if let direction = direction {
if case .animated = transition, let size = self.validLayout {
let offset: CGFloat
switch direction {
case .left:
offset = -size.width
case .right:
offset = size.width
}
previousNode.frame = self.bounds.offsetBy(dx: offset, dy: 0.0)
transition.animateHorizontalOffsetAdditive(node: self, offset: offset, completion: { [weak previousNode] in
previousNode?.removeFromSupernode()
})
} else {
previousNode.removeFromSupernode()
}
} else {
transition.updateAlpha(node: previousNode, alpha: 0.0, completion: { [weak previousNode] _ in
previousNode?.removeFromSupernode()
})
}
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let currentNode = self.currentNode {
return currentNode.view.hitTest(self.view.convert(point, to: currentNode.view), with: event)
}
return nil
}
}
final class ChatListEmptyNode: ASDisplayNode {
let isFilter: Bool
private(set) var isLoading: Bool
@ -174,6 +42,7 @@ final class ChatListEmptyNode: ASDisplayNode {
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.activityIndicator)
let animationName: String
if isFilter {

View file

@ -0,0 +1,475 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import ItemListUI
import CheckNode
import AvatarNode
import AccountContext
import TelegramPresentationData
import ChatListSearchItemHeader
enum ChatListFilterCategoryIcon {
case contacts
case nonContacts
case smallGroups
case largeGroups
case channels
case bots
case muted
case read
case archived
}
final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let title: String
let icon: ChatListFilterCategoryIcon
let isRevealed: Bool
let selectable: Bool = false
let sectionId: ItemListSectionId
let updatedRevealedOptions: (Bool) -> Void
let remove: () -> Void
init(
presentationData: ItemListPresentationData,
title: String,
icon: ChatListFilterCategoryIcon,
isRevealed: Bool,
sectionId: ItemListSectionId,
updatedRevealedOptions: @escaping (Bool) -> Void,
remove: @escaping () -> Void
) {
self.presentationData = presentationData
self.title = title
self.icon = icon
self.isRevealed = isRevealed
self.sectionId = sectionId
self.updatedRevealedOptions = updatedRevealedOptions
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 = ChatListFilterPresetCategoryItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), false)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (.complete(), { _ in apply(synchronousLoads, 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? ChatListFilterPresetCategoryItemNode {
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), false)
Queue.mainQueue().async {
completion(layout, { _ in
apply(false, animated)
})
}
}
}
}
}
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
}
}
private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))
private let badgeFont = Font.regular(15.0)
class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let avatarNode: ASImageNode
private let titleNode: TextNode
private var item: ChatListFilterPresetCategoryItem?
private var layoutParams: ListViewItemLayoutParams?
private var editableControlNode: ItemListEditableControlNode?
override var canBeSelected: Bool {
if self.editableControlNode != nil {
return false
}
return false
}
var tag: ItemListItemTag? {
return nil
}
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.avatarNode = ASImageNode()
self.avatarNode.isUserInteractionEnabled = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.isAccessibilityElement = true
self.addSubnode(self.avatarNode)
self.addSubnode(self.titleNode)
}
override func didLoad() {
super.didLoad()
}
func asyncLayout() -> (_ item: ChatListFilterPresetCategoryItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ headerAtTop: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let currentItem = self.item
return { item, params, neighbors, headerAtTop in
var updatedTheme: PresentationTheme?
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
var titleAttributedString: NSAttributedString?
let peerRevealOptions: [ItemListRevealOption]
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)]
let rightInset: CGFloat = params.rightInset
let titleColor: UIColor
titleColor = item.presentationData.theme.list.itemPrimaryTextColor
titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: titleColor)
let leftInset: CGFloat
let verticalInset: CGFloat
let verticalOffset: CGFloat
let avatarSize: CGFloat
verticalInset = 14.0
verticalOffset = 0.0
avatarSize = 40.0
leftInset = 65.0 + params.leftInset
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
let editingOffset: CGFloat
if false {
let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
} else {
editingOffset = 0.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let insets = itemListNeighborsGroupedInsets(neighbors)
let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0
let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height
let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight))
let separatorHeight = UIScreenPixel
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let hadAvatarImage = self.avatarNode.image != nil
return (layout, { [weak self] synchronousLoad, animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.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
}
var updatedAvatarImage: UIImage?
if !hadAvatarImage {
let color: AvatarBackgroundColor
let imageName: String
switch item.icon {
case .contacts:
color = .blue
imageName = "Chat/Context Menu/User"
case .nonContacts:
color = .yellow
imageName = "Chat/Context Menu/UnknownUser"
case .smallGroups:
color = .green
imageName = "Chat/Context Menu/Groups"
case .largeGroups:
color = .purple
imageName = "Chat/Context Menu/LargeGroup"
case .channels:
color = .red
imageName = "Chat/Context Menu/Channels"
case .bots:
color = .violet
imageName = "Chat/Context Menu/Bots"
case .muted:
color = .red
imageName = "Chat/Context Menu/Muted"
case .read:
color = .blue
imageName = "Chat/Context Menu/Message"
case .archived:
color = .yellow
imageName = "Chat/Context Menu/Archive"
}
updatedAvatarImage = generateAvatarImage(size: CGSize(width: avatarSize, height: avatarSize), icon: generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white), color: color)
}
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.addSubnode(editableControlNode)
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 = true
} 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()
})
}
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: verticalInset + verticalOffset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)))
if let updatedAvatarImage = updatedAvatarImage {
strongSelf.avatarNode.image = updatedAvatarImage
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
strongSelf.backgroundNode.isHidden = false
strongSelf.highlightedBackgroundNode.isHidden = true
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
strongSelf.setRevealOptionsOpened(item.isRevealed, 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 _ = self.item, let params = self.layoutParams else {
return
}
let leftInset: CGFloat
leftInset = 65.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 + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size))
}
override func revealOptionsInteractivelyOpened() {
if let item = self.item {
item.updatedRevealedOptions(true)
}
}
override func revealOptionsInteractivelyClosed() {
if let item = self.item {
item.updatedRevealedOptions(false)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let item = self.item {
item.remove()
}
}
override func header() -> ListViewItemHeader? {
return nil
}
}

View file

@ -14,13 +14,15 @@ import ItemListPeerActionItem
private final class ChatListFilterPresetListControllerArguments {
let context: AccountContext
let addSuggestedPresed: (String, ChatListFilterData) -> Void
let openPreset: (ChatListFilter) -> Void
let addNew: () -> Void
let setItemWithRevealedOptions: (Int32?, Int32?) -> Void
let removePreset: (Int32) -> Void
init(context: AccountContext, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void) {
init(context: AccountContext, addSuggestedPresed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void) {
self.context = context
self.addSuggestedPresed = addSuggestedPresed
self.openPreset = openPreset
self.addNew = addNew
self.setItemWithRevealedOptions = setItemWithRevealedOptions
@ -29,6 +31,8 @@ private final class ChatListFilterPresetListControllerArguments {
}
private enum ChatListFilterPresetListSection: Int32 {
case screenHeader
case suggested
case list
}
@ -45,6 +49,9 @@ private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings
}
private enum ChatListFilterPresetListEntryStableId: Hashable {
case screenHeader
case suggestedListHeader
case suggestedPreset(ChatListFilterData)
case listHeader
case preset(Int32)
case addItem
@ -52,6 +59,9 @@ private enum ChatListFilterPresetListEntryStableId: Hashable {
}
private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case screenHeader(String)
case suggestedListHeader(String)
case suggestedPreset(index: Int, title: String, label: String, preset: ChatListFilterData)
case listHeader(String)
case preset(index: Int, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool)
case addItem(text: String, isEditing: Bool)
@ -59,6 +69,10 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .screenHeader:
return ChatListFilterPresetListSection.screenHeader.rawValue
case .suggestedListHeader, .suggestedPreset:
return ChatListFilterPresetListSection.suggested.rawValue
case .listHeader, .preset, .addItem, .listFooter:
return ChatListFilterPresetListSection.list.rawValue
}
@ -66,19 +80,31 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
var sortId: Int {
switch self {
case .screenHeader:
return 0
case .listHeader:
return 2
return 100
case let .preset(preset):
return 3 + preset.index
return 101 + preset.index
case .addItem:
return 1000
case .listFooter:
return 1001
case .suggestedListHeader:
return 1002
case let .suggestedPreset(suggestedPreset):
return 1003 + suggestedPreset.index
}
}
var stableId: ChatListFilterPresetListEntryStableId {
switch self {
case .screenHeader:
return .screenHeader
case .suggestedListHeader:
return .suggestedListHeader
case let .suggestedPreset(suggestedPreset):
return .suggestedPreset(suggestedPreset.preset)
case .listHeader:
return .listHeader
case let .preset(preset):
@ -97,10 +123,18 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChatListFilterPresetListControllerArguments
switch self {
case let .screenHeader(text):
return ChatListFilterSettingsHeaderItem(theme: presentationData.theme, text: text, sectionId: self.section)
case let .suggestedListHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .suggestedPreset(_, title, label, preset):
return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, title: title, label: label, sectionId: self.section, style: .blocks, installAction: {
arguments.addSuggestedPresed(title, preset)
}, tag: nil)
case let .listHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .preset(index, title, label, preset, canBeReordered, canBeDeleted, isEditing):
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title ?? "", label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, sectionId: self.section, action: {
case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing):
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, sectionId: self.section, action: {
arguments.openPreset(preset)
}, setItemWithRevealedOptions: { lhs, rhs in
arguments.setItemWithRevealedOptions(lhs, rhs)
@ -122,9 +156,20 @@ private struct ChatListFilterPresetListControllerState: Equatable {
var revealedPreset: Int32? = nil
}
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], settings: ChatListFilterSettings) -> [ChatListFilterPresetListEntry] {
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], suggestedFilters: [(String, String, ChatListFilterData)], settings: ChatListFilterSettings) -> [ChatListFilterPresetListEntry] {
var entries: [ChatListFilterPresetListEntry] = []
entries.append(.screenHeader("Create filters for different groups of chats and\nquickly switch between them."))
let filteredSuggestedFilters = suggestedFilters.filter { _, _, data in
for (filter, _) in filters {
if filter.data == data {
return false
}
}
return true
}
entries.append(.listHeader("FILTERS"))
for (filter, chatCount) in filters {
entries.append(.preset(index: entries.count, title: filter.title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing))
@ -134,6 +179,13 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
}
entries.append(.listFooter("Tap \"Edit\" to change the order or delete filters."))
if !filteredSuggestedFilters.isEmpty {
entries.append(.suggestedListHeader("RECOMMENDED FILTERS"))
for (title, label, data) in filteredSuggestedFilters {
entries.append(.suggestedPreset(index: entries.count, title: title, label: label, preset: data))
}
}
return entries
}
@ -147,9 +199,20 @@ public func chatListFilterPresetListController(context: AccountContext, updated:
var dismissImpl: (() -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
let arguments = ChatListFilterPresetListControllerArguments(context: context, openPreset: { preset in
let arguments = ChatListFilterPresetListControllerArguments(context: context,
addSuggestedPresed: { title, data in
let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
settings.filters.insert(ChatListFilter(id: max(2, settings.filters.map({ $0.id + 1 }).max() ?? 2), title: title, data: data), at: 0)
return settings
})
|> deliverOnMainQueue).start(next: { settings in
updated(settings.filters)
let _ = replaceRemoteChatListFilters(account: context.account).start()
})
}, openPreset: { preset in
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: updated))
}, addNew: {
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: updated))
@ -164,7 +227,7 @@ public func chatListFilterPresetListController(context: AccountContext, updated:
}, removePreset: { id in
let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if let index = settings.filters.index(where: { $0.id == id }) {
if let index = settings.filters.firstIndex(where: { $0.id == id }) {
settings.filters.remove(at: index)
}
return settings
@ -207,6 +270,11 @@ public func chatListFilterPresetListController(context: AccountContext, updated:
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
let suggestedFilters: [(String, String, ChatListFilterData)] = [
("Unread", "All unread chats", ChatListFilterData(categories: .all, excludeMuted: false, excludeRead: true, excludeArchived: false, includePeers: [], excludePeers: [])),
("Personal", "Exclude large groups and channels", ChatListFilterData(categories: ChatListFilterPeerCategories.all.subtracting([.largeGroups, .channels]), excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: [], excludePeers: [])),
]
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get(),
@ -238,7 +306,7 @@ public func chatListFilterPresetListController(context: AccountContext, updated:
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Filters"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCounts, settings: filterSettings), style: .blocks, animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCounts, suggestedFilters: suggestedFilters, settings: filterSettings), style: .blocks, animateChanges: true)
return (controllerState, (listState, arguments))
}
@ -253,9 +321,6 @@ public func chatListFilterPresetListController(context: AccountContext, updated:
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()
}

View file

@ -0,0 +1,381 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
public class ChatListFilterPresetListSuggestedItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let title: String
let label: String
public let sectionId: ItemListSectionId
let style: ItemListStyle
let installAction: (() -> Void)?
public let tag: ItemListItemTag?
public init(
presentationData: ItemListPresentationData,
title: String,
label: String,
sectionId: ItemListSectionId,
style: ItemListStyle,
installAction: (() -> Void)?,
tag: ItemListItemTag? = nil
) {
self.presentationData = presentationData
self.title = title
self.label = label
self.sectionId = sectionId
self.style = style
self.installAction = installAction
self.tag = tag
}
public 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 = ChatListFilterPresetListSuggestedItemNode()
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() })
})
}
}
}
public 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? ChatListFilterPresetListSuggestedItemNode {
let makeLayout = nodeValue.asyncLayout()
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()
})
}
}
}
}
}
public var selectable: Bool = false
public func selected(listView: ListView){
}
}
public class ChatListFilterPresetListSuggestedItemNode: ListViewItemNode, ItemListItemNode {
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 labelNode: TextNode
private let buttonBackgroundNode: ASImageNode
private let buttonTitleNode: TextNode
private let buttonNode: HighlightTrackingButtonNode
private let activateArea: AccessibilityAreaNode
private var item: ChatListFilterPresetListSuggestedItem?
override public var canBeSelected: Bool {
return false
}
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.buttonBackgroundNode = ASImageNode()
self.buttonBackgroundNode.isUserInteractionEnabled = false
self.buttonTitleNode = TextNode()
self.buttonTitleNode.isUserInteractionEnabled = false
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.buttonNode = HighlightTrackingButtonNode()
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.buttonBackgroundNode)
self.addSubnode(self.buttonTitleNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.activateArea)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonBackgroundNode.alpha = 0.7
} else {
strongSelf.buttonBackgroundNode.alpha = 1.0
strongSelf.buttonBackgroundNode.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.3)
}
}
}
}
@objc private func buttonPressed() {
self.item?.installAction?()
}
public func asyncLayout() -> (_ item: ChatListFilterPresetListSuggestedItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
let currentItem = self.item
return { item, params, neighbors in
let rightInset: CGFloat
rightInset = 16.0 + params.rightInset
var updatedTheme: PresentationTheme?
var updatedButtonImage: UIImage?
let buttonDiameter: CGFloat = 28.0
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updatedButtonImage = generateStretchableFilledCircleImage(diameter: buttonDiameter, color: item.presentationData.theme.list.itemCheckColors.fillColor)
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let titleColor: UIColor
titleColor = item.presentationData.theme.list.itemPrimaryTextColor
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stickers_Install, font: Font.semibold(14.0), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let additionalTextRightInset: CGFloat = buttonTitleLayout.size.width + 16.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset - additionalTextRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
let labelFont: UIFont
let labelBadgeColor: UIColor
var labelConstrain: CGFloat = params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = detailFont
labelConstrain = params.width - params.rightInset - 40.0 - leftInset
let multilineLabel = false
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor:labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat = 11.0
let titleSpacing: CGFloat = 3.0
let height: CGFloat
height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.label
strongSelf.activateArea.accessibilityTraits = []
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let _ = labelApply()
let _ = buttonTitleApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
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
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 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)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 14.0 * 2.0, height: buttonDiameter)
let buttonFrame = CGRect(origin: CGPoint(x: params.width - rightInset - buttonSize.width, y: floor((layout.contentSize.height - buttonSize.height) / 2.0)), size: buttonSize)
strongSelf.buttonNode.frame = buttonFrame
if let updatedButtonImage = updatedButtonImage {
strongSelf.buttonBackgroundNode.image = updatedButtonImage
}
strongSelf.buttonBackgroundNode.frame = buttonFrame
strongSelf.buttonTitleNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - buttonTitleLayout.size.width) / 2.0), y: buttonFrame.minY + 1.0 + floor((buttonFrame.height - buttonTitleLayout.size.height) / 2.0)), size: buttonTitleLayout.size)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel))
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && false {
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 public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -0,0 +1,124 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import AppBundle
class ChatListFilterSettingsHeaderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let text: String
let sectionId: ItemListSectionId
init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId) {
self.theme = theme
self.text = text
self.sectionId = sectionId
}
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 = ChatListFilterSettingsHeaderItemNode()
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() })
})
}
}
}
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 {
guard let nodeValue = node() as? ChatListFilterSettingsHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
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()
})
}
}
}
}
}
private let titleFont = Font.regular(13.0)
class ChatListFilterSettingsHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private var animationNode: AnimatedStickerNode
private var item: ChatListFilterSettingsHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.animationNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "ChatListFolders", ofType: "tgs") {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
self.animationNode.visibility = true
}
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.animationNode)
}
func asyncLayout() -> (_ item: ChatListFilterSettingsHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, params, neighbors in
let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 92.0
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
let insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string
let iconSize = CGSize(width: 96.0, height: 96.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
}
})
}
}
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)
}
}

View file

@ -260,6 +260,11 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
private var previousSelectedAbsFrame: CGRect?
private var previousSelectedFrame: CGRect?
func cancelAnimations() {
self.selectedLineNode.layer.removeAllAnimations()
self.scrollNode.layer.removeAllAnimations()
}
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, transitionFraction: CGFloat, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter
let previousScrollBounds = self.scrollNode.bounds
@ -393,7 +398,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
var previousFrame: CGRect?
var nextFrame: CGRect?
var selectedFrame: CGRect?
if let selectedFilter = selectedFilter, let currentIndex = filters.index(where: { $0.id == selectedFilter }) {
if let selectedFilter = selectedFilter, let currentIndex = filters.firstIndex(where: { $0.id == selectedFilter }) {
func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t)))
}
@ -423,12 +428,11 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
transition.updateFrame(node: self.selectedLineNode, frame: lineFrame)
}
if !transitionFraction.isZero {
if previousScrollBounds.minX.isZero {
focusOnSelectedFilter = true
} else if previousScrollBounds.maxX == previousScrollBounds.width {
focusOnSelectedFilter = true
} else if let previousSelectedFrame = self.previousSelectedFrame, abs(previousSelectedFrame.offsetBy(dx: -previousScrollBounds.minX, dy: 0.0).midX - previousScrollBounds.width / 2.0) < 1.0 {
focusOnSelectedFilter = true
if let previousSelectedFrame = self.previousSelectedFrame, abs(previousSelectedFrame.offsetBy(dx: -previousScrollBounds.minX, dy: 0.0).midX - previousScrollBounds.width / 2.0) < 1.0 {
let previousContentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(previousSelectedFrame.midX - self.scrollNode.bounds.width / 2.0)))
if abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 {
focusOnSelectedFilter = true
}
}
}
if focusOnSelectedFilter {

View file

@ -1003,6 +1003,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self?.listNode.clearHighlightAnimated(true)
}, disabledPeerSelected: { _ in
}, togglePeerSelected: { _ in
}, additionalCategorySelected: { _ in
}, messageSelected: { [weak self] peer, message, _ in
self?.view.endEditing(true)
if let peer = message.peers[message.id.peerId] {

View file

@ -14,10 +14,11 @@ import ContactsPeerItem
import ContextUI
import ItemListUI
import SearchUI
import ChatListSearchItemHeader
public enum ChatListNodeMode {
case chatList
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool)
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory])
}
struct ChatListNodeListViewTransition {
@ -50,6 +51,7 @@ public final class ChatListNodeInteraction {
let peerSelected: (Peer) -> Void
let disabledPeerSelected: (Peer) -> Void
let togglePeerSelected: (PeerId) -> Void
let additionalCategorySelected: (Int) -> Void
let messageSelected: (Peer, Message, Bool) -> Void
let groupSelected: (PeerGroupId) -> Void
let addContact: (String) -> Void
@ -66,11 +68,12 @@ public final class ChatListNodeInteraction {
public var searchTextHighightState: String?
var highlightedChatLocation: ChatListHighlightedLocation?
public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, messageSelected: @escaping (Peer, Message, Bool) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) {
public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (Peer, Message, Bool) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) {
self.activateSearch = activateSearch
self.peerSelected = peerSelected
self.disabledPeerSelected = disabledPeerSelected
self.togglePeerSelected = togglePeerSelected
self.additionalCategorySelected = additionalCategorySelected
self.messageSelected = messageSelected
self.groupSelected = groupSelected
self.addContact = addContact
@ -103,12 +106,14 @@ public struct ChatListNodeState: Equatable {
public var pendingRemovalPeerIds: Set<PeerId>
public var pendingClearHistoryPeerIds: Set<PeerId>
public var archiveShouldBeTemporaryRevealed: Bool
public var selectedAdditionalCategoryIds: Set<Int>
public init(presentationData: ChatListPresentationData, editing: Bool, peerIdWithRevealedOptions: PeerId?, selectedPeerIds: Set<PeerId>, peerInputActivities: ChatListNodePeerInputActivities?, pendingRemovalPeerIds: Set<PeerId>, pendingClearHistoryPeerIds: Set<PeerId>, archiveShouldBeTemporaryRevealed: Bool) {
public init(presentationData: ChatListPresentationData, editing: Bool, peerIdWithRevealedOptions: PeerId?, selectedPeerIds: Set<PeerId>, selectedAdditionalCategoryIds: Set<Int>, peerInputActivities: ChatListNodePeerInputActivities?, pendingRemovalPeerIds: Set<PeerId>, pendingClearHistoryPeerIds: Set<PeerId>, archiveShouldBeTemporaryRevealed: Bool) {
self.presentationData = presentationData
self.editing = editing
self.peerIdWithRevealedOptions = peerIdWithRevealedOptions
self.selectedPeerIds = selectedPeerIds
self.selectedAdditionalCategoryIds = selectedAdditionalCategoryIds
self.peerInputActivities = peerInputActivities
self.pendingRemovalPeerIds = pendingRemovalPeerIds
self.pendingClearHistoryPeerIds = pendingClearHistoryPeerIds
@ -128,6 +133,9 @@ public struct ChatListNodeState: Equatable {
if lhs.selectedPeerIds != rhs.selectedPeerIds {
return false
}
if lhs.selectedAdditionalCategoryIds != rhs.selectedAdditionalCategoryIds {
return false
}
if lhs.peerInputActivities !== rhs.peerInputActivities {
return false
}
@ -149,11 +157,22 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
switch entry.entry {
case .HeaderEntry:
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd, hasFailedMessages):
case let .AdditionalCategory(_, id, title, image, selected, presentationData):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
context: context,
title: title,
image: image,
isSelected: selected,
action: {
nodeInteraction.additionalCategorySelected(id)
}
), directionHint: entry.directionHint)
case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd, hasFailedMessages, isContact):
switch mode {
case .chatList:
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, isInFilter: isInFilter, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .peers(filter, _):
case let .peers(filter, isSelecting, _):
let itemPeer = peer.chatMainPeer
var chatPeer: Peer?
if let peer = peer.peers[peer.peerId] {
@ -217,8 +236,23 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
}
}
}
var header: ChatListSearchItemHeader?
switch mode {
case let .peers(_, _, additionalCategories):
if !additionalCategories.isEmpty {
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
default:
break
}
var status: ContactsPeerItemStatus = .none
if isSelecting, let itemPeer = itemPeer {
status = .custom(statusStringForPeerType(strings: presentationData.strings, peer: itemPeer, isContact: isContact))
}
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: status, enabled: enabled, selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
if let chatPeer = chatPeer {
nodeInteraction.peerSelected(chatPeer)
}
@ -241,11 +275,11 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId, isInFilter: Bool, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
return entries.map { entry -> ListViewUpdateItem in
switch entry.entry {
case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd, hasFailedMessages):
case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd, hasFailedMessages, isContact):
switch mode {
case .chatList:
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, isInFilter: isInFilter, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .peers(filter, _):
case let .peers(filter, isSelecting, _):
let itemPeer = peer.chatMainPeer
var chatPeer: Peer?
if let peer = peer.peers[peer.peerId] {
@ -266,7 +300,22 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
enabled = false
}
}
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in
var header: ChatListSearchItemHeader?
switch mode {
case let .peers(_, _, additionalCategories):
if !additionalCategories.isEmpty {
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
default:
break
}
var status: ContactsPeerItemStatus = .none
if isSelecting, let itemPeer = itemPeer {
status = .custom(statusStringForPeerType(strings: presentationData.strings, peer: itemPeer, isContact: isContact))
}
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: status, enabled: enabled, selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
if let chatPeer = chatPeer {
nodeInteraction.peerSelected(chatPeer)
}
@ -284,6 +333,17 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
case .HeaderEntry:
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .AdditionalCategory(index: _, id, title, image, selected, presentationData):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
context: context,
title: title,
image: image,
isSelected: selected,
action: {
nodeInteraction.additionalCategorySelected(id)
}
), directionHint: entry.directionHint)
}
}
}
@ -329,11 +389,6 @@ public enum ChatListNodeEmptyState: Equatable {
case empty(isLoading: Bool)
}
enum ChatListNodePaneSwitchAnimationDirection {
case left
case right
}
public final class ChatListNode: ListView {
private let controlsHistoryPreload: Bool
private let context: AccountContext
@ -354,6 +409,7 @@ public final class ChatListNode: ListView {
public var peerSelected: ((Peer, Bool, Bool) -> Void)?
public var disabledPeerSelected: ((Peer) -> Void)?
public var additionalCategorySelected: ((Int) -> Void)?
public var groupSelected: ((PeerGroupId) -> Void)?
public var addContact: ((String) -> Void)?
public var activateSearch: (() -> Void)?
@ -379,16 +435,13 @@ public final class ChatListNode: ListView {
return self.statePromise.get()
}
var paneSwitchAnimation: (ChatListNodePaneSwitchAnimationDirection, ContainedViewLayoutTransition)?
private var currentLocation: ChatListNodeLocation?
private(set) var chatListFilter: ChatListFilter? {
didSet {
self.chatListFilterValue.set(.single(self.chatListFilter))
if self.chatListFilter != oldValue {
if let currentLocation = self.currentLocation {
self.setChatListLocation(.initial(count: 50, filter: self.chatListFilter))
}
self.setChatListLocation(.initial(count: 50, filter: self.chatListFilter))
}
}
}
@ -433,7 +486,7 @@ public final class ChatListNode: ListView {
}
}
var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ChatListNodePaneSwitchAnimationDirection?, ContainedViewLayoutTransition) -> Void)?
var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)?
private var currentIsEmptyState: ChatListNodeEmptyState?
public var addedVisibleChatsWithPeerIds: (([PeerId]) -> Void)?
@ -454,11 +507,11 @@ public final class ChatListNode: ListView {
self.mode = mode
var isSelecting = false
if case .peers(_, true) = mode {
if case .peers(_, true, _) = mode {
isSelecting = true
}
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false)
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false)
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
self.theme = theme
@ -494,6 +547,8 @@ public final class ChatListNode: ListView {
}
return state
}
}, additionalCategorySelected: { [weak self] id in
self?.additionalCategorySelected?(id)
}, messageSelected: { [weak self] peer, message, isAd in
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
peerSelected(peer, true, isAd)
@ -591,7 +646,7 @@ public final class ChatListNode: ListView {
let currentRemovingPeerId = self.currentRemovingPeerId
let savedMessagesPeer: Signal<Peer?, NoError>
if case let .peers(filter, _) = mode, filter.contains(.onlyWriteable) {
if case let .peers(filter, _, _) = mode, filter.contains(.onlyWriteable) {
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map(Optional.init)
} else {
@ -640,11 +695,11 @@ public final class ChatListNode: ListView {
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.view, state: state, savedMessagesPeer: savedMessagesPeer, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode)
let entries = rawEntries.filter { entry in
switch entry {
case let .PeerEntry(_, _, _, _, _, _, peer, _, _, _, _, _, _, _, _):
case let .PeerEntry(_, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _):
switch mode {
case .chatList:
return true
case let .peers(filter, _):
case let .peers(filter, _, _):
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false }
@ -750,7 +805,7 @@ public final class ChatListNode: ListView {
var didIncludeHiddenByDefaultArchive = false
if let previous = previousView {
for entry in previous.filteredEntries {
if case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = entry {
if case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = entry {
if index.pinningIndex != nil {
previousPinnedChats.append(index.messageIndex.id.peerId)
}
@ -784,6 +839,9 @@ public final class ChatListNode: ListView {
if previousState.selectedPeerIds != state.selectedPeerIds {
disableAnimations = false
}
if previousState.selectedAdditionalCategoryIds != state.selectedAdditionalCategoryIds {
disableAnimations = false
}
if doesIncludeRemovingPeerId != didIncludeRemovingPeerId {
disableAnimations = false
}
@ -828,10 +886,10 @@ public final class ChatListNode: ListView {
let originalView = chatListView.originalView
if let range = range.loadedRange {
var location: ChatListNodeLocation?
if range.firstIndex < 5 && originalView.laterIndex != nil {
location = .navigation(index: originalView.entries[originalView.entries.count - 1].index, filter: strongSelf.chatListFilter)
} else if range.firstIndex >= 5 && range.lastIndex >= originalView.entries.count - 5 && originalView.earlierIndex != nil {
location = .navigation(index: originalView.entries[0].index, filter: strongSelf.chatListFilter)
if range.firstIndex < 5, let laterIndex = originalView.laterIndex {
location = .navigation(index: laterIndex, filter: strongSelf.chatListFilter)
} else if range.firstIndex >= 5, range.lastIndex >= originalView.entries.count - 5, let earlierIndex = originalView.earlierIndex {
location = .navigation(index: earlierIndex, filter: strongSelf.chatListFilter)
}
if let location = location, location != strongSelf.currentLocation {
@ -852,7 +910,7 @@ public final class ChatListNode: ListView {
continue
}
switch chatListView.filteredEntries[entryCount - i - 1] {
case let .PeerEntry(_, _, _, readState, notificationSettings, _, _, _, _, _, _, _, _, _, _):
case let .PeerEntry(_, _, _, readState, notificationSettings, _, _, _, _, _, _, _, _, _, _, _):
if let readState = readState {
let count = readState.count
rawUnreadCount += count
@ -998,7 +1056,7 @@ public final class ChatListNode: ListView {
var referenceId: PinnedItemId?
var beforeAll = false
switch toEntry {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, isAd, _):
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, isAd, _, _):
if isAd {
beforeAll = true
} else {
@ -1010,13 +1068,13 @@ public final class ChatListNode: ListView {
break
}
if let _ = fromEntry.sortIndex.pinningIndex {
if case let .index(index) = fromEntry.sortIndex, let _ = index.pinningIndex {
return strongSelf.context.account.postbox.transaction { transaction -> Bool in
var itemIds = transaction.getPinnedItemIds(groupId: groupId)
var itemId: PinnedItemId?
switch fromEntry {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
itemId = .peer(index.messageIndex.id.peerId)
/*case let .GroupReferenceEntry(_, _, groupId, _, _, _, _):
itemId = .group(groupId)*/
@ -1268,13 +1326,6 @@ public final class ChatListNode: ListView {
if let (transition, completion) = self.enqueuedTransition {
self.enqueuedTransition = nil
let paneSwitchCopyView: UIView?
if let (direction, transition) = self.paneSwitchAnimation, let copyView = self.view.snapshotContentTree(unhide: false, keepTransform: true) {
paneSwitchCopyView = copyView
} else {
paneSwitchCopyView = nil
}
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
if let strongSelf = self {
strongSelf.chatListView = transition.chatListView
@ -1283,7 +1334,7 @@ public final class ChatListNode: ListView {
if case .chatList = strongSelf.mode {
let entryCount = transition.chatListView.filteredEntries.count
if entryCount >= 1 {
if transition.chatListView.filteredEntries[entryCount - 1].sortIndex.pinningIndex != nil {
if case let .index(index) = transition.chatListView.filteredEntries[entryCount - 1].sortIndex, index.pinningIndex != nil {
pinnedOverscroll = true
}
}
@ -1339,11 +1390,11 @@ public final class ChatListNode: ListView {
var containsChats = false
loop: for entry in transition.chatListView.filteredEntries {
switch entry {
case .GroupReferenceEntry, .HoleEntry, .PeerEntry:
containsChats = true
break loop
case .ArchiveIntro, .HeaderEntry:
break
case .GroupReferenceEntry, .HoleEntry, .PeerEntry:
containsChats = true
break loop
case .ArchiveIntro, .HeaderEntry, .AdditionalCategory:
break
}
}
isEmptyState = .notEmpty(containsChats: containsChats)
@ -1364,33 +1415,14 @@ public final class ChatListNode: ListView {
strongSelf.addedVisibleChatsWithPeerIds?(insertedPeerIds)
}
var isEmptyUpdate: (ChatListNodePaneSwitchAnimationDirection?, ContainedViewLayoutTransition) = (nil, .immediate)
if let (direction, transition) = strongSelf.paneSwitchAnimation {
strongSelf.paneSwitchAnimation = nil
if let copyView = paneSwitchCopyView {
let offset: CGFloat
switch direction {
case .left:
offset = -strongSelf.bounds.width
case .right:
offset = strongSelf.bounds.width
}
copyView.frame = strongSelf.bounds.offsetBy(dx: offset, dy: 0.0)
strongSelf.view.addSubview(copyView)
transition.animateHorizontalOffsetAdditive(node: strongSelf, offset: offset, completion: { [weak copyView] in
copyView?.removeFromSuperview()
})
isEmptyUpdate = (direction, transition)
}
} else {
if transition.options.contains(.AnimateInsertion) {
isEmptyUpdate.1 = .animated(duration: 0.25, curve: .easeInOut)
}
var isEmptyUpdate: ContainedViewLayoutTransition = .immediate
if transition.options.contains(.AnimateInsertion) {
isEmptyUpdate = .animated(duration: 0.25, curve: .easeInOut)
}
if strongSelf.currentIsEmptyState != isEmptyState {
strongSelf.currentIsEmptyState = isEmptyState
strongSelf.isEmptyUpdated?(isEmptyState, transition.chatListView.filter != nil, isEmptyUpdate.0, isEmptyUpdate.1)
strongSelf.isEmptyUpdated?(isEmptyState, transition.chatListView.filter != nil, isEmptyUpdate)
}
if !strongSelf.hasUpdatedAppliedChatListFilterValueOnce || transition.chatListView.filter != strongSelf.currentAppliedChatListFilterValue {
@ -1429,7 +1461,7 @@ public final class ChatListNode: ListView {
var isNavigationHidden: Bool {
switch self.visibleContentOffset() {
case let .known(value) where abs(value) < navigationBarSearchContentHeight:
case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0:
return false
default:
return true
@ -1568,7 +1600,7 @@ public final class ChatListNode: ListView {
continue
}
switch chatListView.filteredEntries[entryCount - i - 1] {
case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _):
case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _):
if interaction.highlightedChatLocation?.location == ChatLocation.peer(peer.peerId) {
current = (index, peer.peer!, entryCount - i - 1)
break outer
@ -1614,10 +1646,10 @@ public final class ChatListNode: ListView {
case .previous(unread: false), .next(unread: false):
var target: (ChatListIndex, Peer)? = nil
if let current = current, entryCount > 1 {
if current.2 > 0, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 - 1] {
if current.2 > 0, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 - 1] {
next = (index, peer.peer!)
}
if current.2 <= entryCount - 2, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 + 1] {
if current.2 <= entryCount - 2, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 + 1] {
previous = (index, peer.peer!)
}
if case .previous = option {
@ -1626,7 +1658,7 @@ public final class ChatListNode: ListView {
target = next
}
} else if entryCount > 0 {
if case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[entryCount - 1] {
if case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[entryCount - 1] {
target = (index, peer.peer!)
}
}
@ -1659,7 +1691,7 @@ public final class ChatListNode: ListView {
|> take(1)
|> deliverOnMainQueue).start(next: { update in
let entries = update.view.entries
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _) = entries[10 - index - 1] {
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _, _) = entries[10 - index - 1] {
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter)
self.setChatListLocation(location)
self.peerSelected?(renderedPeer.peer!, false, false)
@ -1702,7 +1734,7 @@ public final class ChatListNode: ListView {
continue
}
switch chatListView.filteredEntries[entryCount - i - 1] {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return index
default:
break
@ -1712,3 +1744,31 @@ public final class ChatListNode: ListView {
return nil
}
}
//TODO:localize
private func statusStringForPeerType(strings: PresentationStrings, peer: Peer, isContact: Bool) -> String {
if let user = peer as? TelegramUser {
if user.botInfo != nil || user.flags.contains(.isSupport) {
return "bot"
} else if isContact {
return "contact"
} else {
return "user"
}
} else if peer is TelegramSecretChat {
if isContact {
return "contact"
} else {
return "user"
}
} else if peer is TelegramGroup {
return "group"
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
return "group"
} else {
return "channel"
}
}
return "user"
}

View file

@ -1,4 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import SyncCore
@ -11,27 +12,55 @@ enum ChatListNodeEntryId: Hashable {
case PeerId(Int64)
case GroupId(PeerGroupId)
case ArchiveIntro
case additionalCategory(Int)
}
enum ChatListNodeEntrySortIndex: Comparable {
case index(ChatListIndex)
case additionalCategory(Int)
static func <(lhs: ChatListNodeEntrySortIndex, rhs: ChatListNodeEntrySortIndex) -> Bool {
switch lhs {
case let .index(lhsIndex):
switch rhs {
case let .index(rhsIndex):
return lhsIndex < rhsIndex
case .additionalCategory:
return false
}
case let .additionalCategory(lhsIndex):
switch rhs {
case let .additionalCategory(rhsIndex):
return lhsIndex < rhsIndex
case .index:
return true
}
}
}
}
enum ChatListNodeEntry: Comparable, Identifiable {
case HeaderEntry
case PeerEntry(index: ChatListIndex, presentationData: ChatListPresentationData, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, presence: PeerPresence?, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool, selected: Bool, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool, hasFailedMessages: Bool)
case PeerEntry(index: ChatListIndex, presentationData: ChatListPresentationData, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, presence: PeerPresence?, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool, selected: Bool, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool, hasFailedMessages: Bool, isContact: Bool)
case HoleEntry(ChatListHole, theme: PresentationTheme)
case GroupReferenceEntry(index: ChatListIndex, presentationData: ChatListPresentationData, groupId: PeerGroupId, peers: [ChatListGroupReferencePeer], message: Message?, editing: Bool, unreadState: PeerGroupUnreadCountersCombinedSummary, revealed: Bool, hiddenByDefault: Bool)
case ArchiveIntro(presentationData: ChatListPresentationData)
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, selected: Bool, presentationData: ChatListPresentationData)
var sortIndex: ChatListIndex {
var sortIndex: ChatListNodeEntrySortIndex {
switch self {
case .HeaderEntry:
return ChatListIndex.absoluteUpperBound
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return index
return .index(ChatListIndex.absoluteUpperBound)
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return .index(index)
case let .HoleEntry(hole, _):
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
return .index(ChatListIndex(pinningIndex: nil, messageIndex: hole.index))
case let .GroupReferenceEntry(index, _, _, _, _, _, _, _, _):
return index
return .index(index)
case .ArchiveIntro:
return ChatListIndex.absoluteUpperBound.successor
return .index(ChatListIndex.absoluteUpperBound.successor)
case let .AdditionalCategory(additionalCategory):
return .additionalCategory(additionalCategory.index)
}
}
@ -39,7 +68,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
switch self {
case .HeaderEntry:
return .Header
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return .PeerId(index.messageIndex.id.peerId.toInt64())
case let .HoleEntry(hole, _):
return .Hole(Int64(hole.index.id.id))
@ -47,6 +76,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
return .GroupId(groupId)
case .ArchiveIntro:
return .ArchiveIntro
case let .AdditionalCategory(additionalCategory):
return .additionalCategory(additionalCategory.id)
}
}
@ -62,9 +93,9 @@ enum ChatListNodeEntry: Comparable, Identifiable {
} else {
return false
}
case let .PeerEntry(lhsIndex, lhsPresentationData, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsSelected, lhsInputActivities, lhsAd, lhsHasFailedMessages):
case let .PeerEntry(lhsIndex, lhsPresentationData, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsSelected, lhsInputActivities, lhsAd, lhsHasFailedMessages, lhsIsContact):
switch rhs {
case let .PeerEntry(rhsIndex, rhsPresentationData, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsSelected, rhsInputActivities, rhsAd, rhsHasFailedMessages):
case let .PeerEntry(rhsIndex, rhsPresentationData, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsSelected, rhsInputActivities, rhsAd, rhsHasFailedMessages, rhsIsContact):
if lhsIndex != rhsIndex {
return false
}
@ -148,6 +179,9 @@ enum ChatListNodeEntry: Comparable, Identifiable {
if lhsHasFailedMessages != rhsHasFailedMessages {
return false
}
if lhsIsContact != rhsIsContact {
return false
}
return true
default:
return false
@ -201,6 +235,30 @@ enum ChatListNodeEntry: Comparable, Identifiable {
} else {
return false
}
case let .AdditionalCategory(lhsIndex, lhsId, lhsTitle, lhsImage, lhsSelected, lhsPresentationData):
if case let .AdditionalCategory(rhsIndex, rhsId, rhsTitle, rhsImage, rhsSelected, rhsPresentationData) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsId != rhsId {
return false
}
if lhsTitle != rhsTitle {
return false
}
if lhsImage !== rhsImage {
return false
}
if lhsSelected != rhsSelected {
return false
}
if lhsPresentationData !== rhsPresentationData {
return false
}
return true
} else {
return false
}
}
}
}
@ -220,7 +278,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
if view.laterIndex == nil, case .chatList = mode {
var groupEntryCount = 0
for groupReference in view.groupEntries {
for _ in view.groupEntries {
groupEntryCount += 1
}
pinnedIndexOffset += UInt16(groupEntryCount)
@ -231,7 +289,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
}
loop: for entry in view.entries {
switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
if let savedMessagesPeer = savedMessagesPeer, savedMessagesPeer.id == index.messageIndex.id.peerId {
continue loop
}
@ -244,7 +302,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
updatedMessage = nil
updatedCombinedReadState = nil
}
result.append(.PeerEntry(index: offsetPinnedIndex(index, offset: pinnedIndexOffset), presentationData: state.presentationData, message: updatedMessage, readState: updatedCombinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, presence: peerPresence, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, selected: state.selectedPeerIds.contains(index.messageIndex.id.peerId), inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: false, hasFailedMessages: hasFailed))
result.append(.PeerEntry(index: offsetPinnedIndex(index, offset: pinnedIndexOffset), presentationData: state.presentationData, message: updatedMessage, readState: updatedCombinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, presence: peerPresence, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, selected: state.selectedPeerIds.contains(index.messageIndex.id.peerId), inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: false, hasFailedMessages: hasFailed, isContact: isContact))
case let .HoleEntry(hole):
if hole.index.timestamp == Int32.max - 1 {
return ([], true)
@ -256,13 +314,13 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
var pinningIndex: UInt16 = UInt16(pinnedIndexOffset == 0 ? 0 : (pinnedIndexOffset - 1))
if let savedMessagesPeer = savedMessagesPeer {
result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, message: nil, readState: nil, notificationSettings: nil, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, selected: false, inputActivities: nil, isAd: false, hasFailedMessages: false))
result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, message: nil, readState: nil, notificationSettings: nil, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, selected: false, inputActivities: nil, isAd: false, hasFailedMessages: false, isContact: false))
} else {
if !view.additionalItemEntries.isEmpty {
for entry in view.additionalItemEntries.reversed() {
switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
result.append(.PeerEntry(index: ChatListIndex(pinningIndex: pinningIndex, messageIndex: index.messageIndex), presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, presence: peerPresence, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, selected: state.selectedPeerIds.contains(index.messageIndex.id.peerId), inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: true, hasFailedMessages: hasFailed))
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
result.append(.PeerEntry(index: ChatListIndex(pinningIndex: pinningIndex, messageIndex: index.messageIndex), presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, presence: peerPresence, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, selected: state.selectedPeerIds.contains(index.messageIndex.id.peerId), inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: true, hasFailedMessages: hasFailed, isContact: isContact))
if pinningIndex != 0 {
pinningIndex -= 1
}
@ -288,6 +346,14 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
result.append(.HeaderEntry)
}
if view.laterIndex == nil, case let .peers(_, _, additionalCategories) = mode {
var index = 0
for category in additionalCategories.reversed(){
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
index += 1
}
}
}
if result.count >= 1, case .HoleEntry = result[result.count - 1] {

View file

@ -31,7 +31,8 @@ struct ChatListNodeViewUpdate {
func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredicate {
let includePeers = Set(filter.includePeers)
return ChatListFilterPredicate(includePeerIds: includePeers, include: { peer, notificationSettings, isUnread in
let excludePeers = Set(filter.excludePeers)
return ChatListFilterPredicate(includePeerIds: includePeers, excludePeerIds: excludePeers, include: { peer, notificationSettings, isUnread, isContact, isArchived in
if filter.excludeRead {
if !isUnread {
return false
@ -46,15 +47,26 @@ func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredic
return false
}
}
if !filter.categories.contains(.privateChats) {
if filter.excludeArchived {
if isArchived {
return false
}
}
if !filter.categories.contains(.contacts) && isContact {
if let user = peer as? TelegramUser {
if user.botInfo == nil {
return false
}
} else if let _ = peer as? TelegramSecretChat {
return false
}
}
if !filter.categories.contains(.secretChats) {
if let _ = peer as? TelegramSecretChat {
if !filter.categories.contains(.nonContacts) && !isContact {
if let user = peer as? TelegramUser {
if user.botInfo == nil {
return false
}
} else if let _ = peer as? TelegramSecretChat {
return false
}
}
@ -65,7 +77,7 @@ func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredic
}
}
}
if !filter.categories.contains(.privateGroups) {
if !filter.categories.contains(.smallGroups) {
if let _ = peer as? TelegramGroup {
return false
} else if let channel = peer as? TelegramChannel {
@ -76,7 +88,7 @@ func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredic
}
}
}
if !filter.categories.contains(.publicGroups) {
if !filter.categories.contains(.largeGroups) {
if let channel = peer as? TelegramChannel {
if case .group = channel.info {
if channel.username != nil {

View file

@ -89,8 +89,8 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV
var minTimestamp: Int32?
var maxTimestamp: Int32?
for (_, item, _) in indicesAndItems {
if case .PeerEntry = item, item.sortIndex.pinningIndex == nil {
let timestamp = item.sortIndex.messageIndex.timestamp
if case .PeerEntry = item, case let .index(index) = item.sortIndex, index.pinningIndex == nil {
let timestamp = index.messageIndex.timestamp
if minTimestamp == nil || timestamp < minTimestamp! {
minTimestamp = timestamp
@ -146,7 +146,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV
case let .index(scrollIndex, position, directionHint, animated):
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if entry.sortIndex >= scrollIndex {
if entry.sortIndex >= .index(scrollIndex) {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint)
break
}
@ -156,7 +156,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV
if scrollToItem == nil {
var index = 0
for entry in toView.filteredEntries.reversed() {
if entry.sortIndex < scrollIndex {
if entry.sortIndex < .index(scrollIndex) {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint)
break
}

View file

@ -10,300 +10,6 @@ import Postbox
import TelegramUIPreferences
import TelegramCore
final class TabBarChatListFilterController: ViewController {
private var controllerNode: TabBarChatListFilterControllerNode {
return self.displayNode as! TabBarChatListFilterControllerNode
}
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private let context: AccountContext
private let sourceNodes: [ASDisplayNode]
private let presetList: [ChatListFilter]
private let currentPreset: ChatListFilter?
private let setup: () -> Void
private let updatePreset: (ChatListFilter?) -> Void
private var presentationData: PresentationData
private var didPlayPresentationAnimation = false
private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, sourceNodes: [ASDisplayNode], presetList: [ChatListFilter], currentPreset: ChatListFilter?, setup: @escaping () -> Void, updatePreset: @escaping (ChatListFilter?) -> Void) {
self.context = context
self.sourceNodes = sourceNodes
self.presetList = presetList
self.currentPreset = currentPreset
self.setup = setup
self.updatePreset = updatePreset
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.statusBar.ignoreInCall = true
self.lockOrientation = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func loadDisplayNode() {
self.displayNode = TabBarChatListFilterControllerNode(context: self.context, presentationData: self.presentationData, cancel: { [weak self] in
self?.dismiss()
}, 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()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
self.hapticFeedback.impact()
self.controllerNode.animateIn()
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.dismiss(sourceNodes: [], fadeOutIcon: false)
}
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)
})
}
}
private let animationDurationFactor: Double = 1.0
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: ChatListFilter?
private let isCurrent: Bool
private let presentationData: PresentationData
private let action: () -> Bool
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let buttonNode: HighlightTrackingButtonNode
private let titleNode: ImmediateTextNode
private let checkNode: ASImageNode
private let badgeBackgroundNode: ASImageNode
private let badgeTitleNode: ImmediateTextNode
private var badgeText: String = ""
init(context: AccountContext, title: String, preset: ChatListFilter?, 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
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: title, font: Font.regular(17.0), textColor: presentationData.theme.actionSheet.primaryTextColor)
self.checkNode = ASImageNode()
self.checkNode.image = generateItemListCheckIcon(color: presentationData.theme.actionSheet.primaryTextColor)
self.checkNode.isHidden = true//!isCurrent
self.badgeBackgroundNode = ASImageNode()
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: presentationData.theme.list.itemCheckColors.fillColor)
self.badgeTitleNode = ImmediateTextNode()
self.badgeBackgroundNode.isHidden = true
self.badgeTitleNode.isHidden = true
super.init()
self.addSubnode(self.separatorNode)
self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.checkNode)
self.addSubnode(self.badgeBackgroundNode)
self.addSubnode(self.badgeTitleNode)
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 badgeTitleSize = self.badgeTitleNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude))
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(20.0, badgeSize.width + 20.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.checkNode.image {
self.checkNode.frame = CGRect(origin: CGPoint(x: width - rightInset + floor((rightInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size)
}
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: width - rightInset + floor((rightInset - badgeSize.width) / 2.0), y: floor((height - badgeSize.height) / 2.0)), size: badgeSize)
self.badgeBackgroundNode.frame = badgeBackgroundFrame
self.badgeTitleNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeTitleSize.width) / 2.0), y: badgeBackgroundFrame.minY + floor((badgeBackgroundFrame.height - badgeTitleSize.height) / 2.0)), size: badgeTitleSize)
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() {
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
}
}
}
func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilter, Int)]), NoError> {
let preferencesKey: PostboxViewKey = .preferences(keys: [PreferencesKeys.chatListFilters])
return context.account.postbox.combinedView(keys: [preferencesKey])
@ -351,8 +57,8 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
}
let type: RenderedTotalUnreadCountType
switch inAppSettings.totalUnreadCountDisplayStyle {
case .filtered:
type = .filtered
case .filtered:
type = .filtered
}
var result: [(ChatListFilter, Int)] = []
@ -367,7 +73,7 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
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)
let tag = context.account.postbox.seedConfiguration.peerSummaryCounterTags(peer, peerView.isContact)
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings, case .muted = notificationSettings.muteState {
peerTagAndCount[peerId] = (tag, 0)
} else {
@ -387,24 +93,23 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
totalBadge = Int(totalState.count(for: inAppSettings.totalUnreadCountDisplayStyle.category, in: inAppSettings.totalUnreadCountDisplayCategory.statsType, with: inAppSettings.totalUnreadCountIncludeTags))
}
var shouldUpdateLayout = false
for filter in filters {
var tags: [PeerSummaryCounterTags] = []
if filter.data.categories.contains(.privateChats) {
tags.append(.privateChat)
if filter.data.categories.contains(.contacts) {
tags.append(.contact)
}
if filter.data.categories.contains(.secretChats) {
tags.append(.secretChat)
if filter.data.categories.contains(.nonContacts) {
tags.append(.nonContact)
}
if filter.data.categories.contains(.privateGroups) {
tags.append(.privateGroup)
if filter.data.categories.contains(.smallGroups) {
tags.append(.smallGroup)
}
if filter.data.categories.contains(.largeGroups) {
tags.append(.largeGroup)
}
if filter.data.categories.contains(.bots) {
tags.append(.bot)
}
if filter.data.categories.contains(.publicGroups) {
tags.append(.publicGroup)
}
if filter.data.categories.contains(.channels) {
tags.append(.channel)
}
@ -433,399 +138,3 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt
}
}
}
private final class TabBarChatListFilterControllerNode: ViewControllerTracingNode {
private let presentationData: PresentationData
private let cancel: () -> Void
private let effectView: UIVisualEffectView
private var propertyAnimator: AnyObject?
private var displayLinkAnimator: DisplayLinkAnimator?
private let dimNode: ASDisplayNode
private let contentContainerNode: ASDisplayNode
private let contentNodes: [ASDisplayNode & AbstractTabBarChatListFilterItemNode]
private var sourceNodes: [ASDisplayNode]
private var snapshotViews: [UIView] = []
private var validLayout: ContainerViewLayout?
private var countsDisposable: Disposable?
let isReady = Promise<Bool>()
private var didSetIsReady = false
init(context: AccountContext, presentationData: PresentationData, cancel: @escaping () -> Void, sourceNodes: [ASDisplayNode], presetList: [ChatListFilter], currentPreset: ChatListFilter?, setup: @escaping () -> Void, updatePreset: @escaping (ChatListFilter?) -> Void) {
self.presentationData = presentationData
self.cancel = cancel
self.sourceNodes = sourceNodes
self.effectView = UIVisualEffectView()
if #available(iOS 9.0, *) {
} else {
if presentationData.theme.rootController.keyboardColor == .dark {
self.effectView.effect = UIBlurEffect(style: .dark)
} else {
self.effectView.effect = UIBlurEffect(style: .light)
}
self.effectView.alpha = 0.0
}
self.dimNode = ASDisplayNode()
self.dimNode.alpha = 1.0
if presentationData.theme.rootController.keyboardColor == .light {
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.04)
} else {
self.dimNode.backgroundColor = presentationData.theme.chatList.backgroundColor.withAlphaComponent(0.2)
}
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
self.contentContainerNode.cornerRadius = 20.0
self.contentContainerNode.clipsToBounds = true
var contentNodes: [ASDisplayNode & AbstractTabBarChatListFilterItemNode] = []
contentNodes.append(AddFilterItemNode(displaySeparator: true, presentationData: presentationData, action: {
setup()
}))
for i in 0 ..< presetList.count {
let preset = presetList[i]
let title: String = preset.title ?? ""
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
super.init()
self.view.addSubview(self.effectView)
self.addSubnode(self.dimNode)
self.addSubnode(self.contentContainerNode)
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.data.includePeers)
}
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.data.categories.contains(.privateChats) {
tags.append(.privateChat)
}
if preset.data.categories.contains(.secretChats) {
tags.append(.secretChat)
}
if preset.data.categories.contains(.privateGroups) {
tags.append(.privateGroup)
}
if preset.data.categories.contains(.bots) {
tags.append(.bot)
}
if preset.data.categories.contains(.publicGroups) {
tags.append(.publicGroup)
}
if preset.data.categories.contains(.channels) {
tags.append(.channel)
}
var count = 0
if let totalState = totalState {
for tag in tags {
if let value = totalState.filteredCounters[tag] {
count += Int(value.chatCount)
}
}
}
for peerId in preset.data.includePeers {
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 {
if let propertyAnimator = self.propertyAnimator {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator
propertyAnimator?.stopAnimation(true)
}
}
self.countsDisposable?.dispose()
}
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if #available(iOS 10.0, *) {
if let propertyAnimator = self.propertyAnimator {
let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator
propertyAnimator?.stopAnimation(true)
}
self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2 * animationDurationFactor, curve: .easeInOut, animations: { [weak self] in
self?.effectView.effect = makeCustomZoomBlurEffect()
})
}
if let _ = self.propertyAnimator {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor, from: 0.0, to: 1.0, update: { [weak self] value in
(self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value
}, completion: {
})
}
} else {
UIView.animate(withDuration: 0.2 * animationDurationFactor, animations: {
self.effectView.effect = makeCustomZoomBlurEffect()
}, completion: { _ in
})
}
self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let _ = self.validLayout, let sourceNode = self.sourceNodes.first {
let sourceFrame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
self.contentContainerNode.layer.animateFrame(from: sourceFrame, to: self.contentContainerNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
for sourceNode in self.sourceNodes {
if let imageNode = sourceNode as? ASImageNode {
let snapshot = UIImageView()
snapshot.image = imageNode.image
snapshot.frame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
snapshot.isUserInteractionEnabled = false
self.view.addSubview(snapshot)
self.snapshotViews.append(snapshot)
} else if let snapshot = sourceNode.view.snapshotContentTree() {
snapshot.frame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
snapshot.isUserInteractionEnabled = false
self.view.addSubview(snapshot)
self.snapshotViews.append(snapshot)
}
sourceNode.alpha = 0.0
}
}
func animateOut(sourceNodes: [ASDisplayNode], fadeOutIcon: Bool, completion: @escaping () -> Void) {
self.isUserInteractionEnabled = false
var completedEffect = false
var completedSourceNodes = false
let intermediateCompletion: () -> Void = {
if completedEffect && completedSourceNodes {
completion()
}
}
if #available(iOS 10.0, *) {
if let propertyAnimator = self.propertyAnimator {
let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator
propertyAnimator?.stopAnimation(true)
}
self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut, animations: { [weak self] in
self?.effectView.effect = nil
})
}
if let _ = self.propertyAnimator {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor, from: 0.0, to: 0.999, update: { [weak self] value in
(self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value
}, completion: { [weak self] in
if let strongSelf = self {
for sourceNode in strongSelf.sourceNodes {
sourceNode.alpha = 1.0
}
}
completedEffect = true
intermediateCompletion()
})
}
self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.05 * animationDurationFactor, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
} else {
UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: {
if #available(iOS 9.0, *) {
self.effectView.effect = nil
} else {
self.effectView.alpha = 0.0
}
}, completion: { [weak self] _ in
if let strongSelf = self {
for sourceNode in strongSelf.sourceNodes {
sourceNode.alpha = 1.0
}
}
completedEffect = true
intermediateCompletion()
})
}
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { _ in
})
if let _ = self.validLayout, let sourceNode = self.sourceNodes.first {
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)
}
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) {
self.validLayout = layout
transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let sideInset: CGFloat = 18.0
var contentSize = CGSize()
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)
applyNodes.append((itemNode, height, apply))
contentSize.width = max(contentSize.width, width)
contentSize.height += height
}
let insets = layout.insets(options: .input)
let contentOrigin: CGPoint
if let sourceNode = self.sourceNodes.first, let screenFrame = sourceNode.supernode?.convert(sourceNode.frame, to: nil) {
contentOrigin = CGPoint(x: max(16.0, screenFrame.maxX - contentSize.width + 8.0), y: layout.size.height - 66.0 - insets.bottom - contentSize.height)
} else {
contentOrigin = CGPoint(x: max(16.0, layout.size.width - sideInset - contentSize.width), y: layout.size.height - 66.0 - layout.intrinsicInsets.bottom - contentSize.height)
}
transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: contentOrigin, size: contentSize))
var nextY: CGFloat = 0.0
for (itemNode, height, apply) in applyNodes {
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: nextY), size: CGSize(width: contentSize.width, height: height)))
apply(contentSize.width)
nextY += height
}
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel()
}
}
}
private func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView) {
var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x,
y: view.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x,
y: view.bounds.size.height * view.layer.anchorPoint.y)
newPoint = newPoint.applying(view.transform)
oldPoint = oldPoint.applying(view.transform)
var position = view.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
view.layer.position = position
view.layer.anchorPoint = anchorPoint
}

View file

@ -456,8 +456,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
var leftInset: CGFloat = 65.0 + params.leftInset
let rightInset: CGFloat = 10.0 + params.rightInset
let leftInset: CGFloat = 65.0 + params.leftInset
var rightInset: CGFloat = 10.0 + params.rightInset
let updatedSelectionNode: CheckNode?
var isSelected = false
@ -465,7 +465,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
case .none:
updatedSelectionNode = nil
case let .selectable(selected):
leftInset += 28.0
rightInset += 28.0
isSelected = selected
let selectionNode: CheckNode
@ -849,7 +849,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
updatedSelectionNode.setIsChecked(isSelected, animated: animated)
updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 6.0, y: floor((nodeLayout.contentSize.height - 32.0) / 2.0)), size: CGSize(width: 32.0, height: 32.0))
updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 32.0 - 12.0, y: floor((nodeLayout.contentSize.height - 32.0) / 2.0)), size: CGSize(width: 32.0, height: 32.0))
} else if let selectionNode = strongSelf.selectionNode {
selectionNode.removeFromSupernode()
strongSelf.selectionNode = nil

View file

@ -207,6 +207,24 @@ public func generateFilledCircleImage(diameter: CGFloat, color: UIColor?, stroke
})
}
public func generateAdjustedStretchableFilledCircleImage(diameter: CGFloat, color: UIColor) -> UIImage? {
let corner: CGFloat = diameter / 2.0
return generateImage(CGSize(width: diameter + 2.0, height: diameter + 2.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.move(to: CGPoint(x: 0.0, y: corner))
context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: corner, y: 0.0), radius: corner)
context.addLine(to: CGPoint(x: size.width - corner, y: 0.0))
context.addArc(tangent1End: CGPoint(x: size.width, y: 0.0), tangent2End: CGPoint(x: size.width, y: corner), radius: corner)
context.addLine(to: CGPoint(x: size.width, y: size.height - corner))
context.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - corner, y: size.height), radius: corner)
context.addLine(to: CGPoint(x: corner, y: size.height))
context.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - corner), radius: corner)
context.closePath()
context.fillPath()
})?.stretchableImage(withLeftCapWidth: Int(diameter / 2) + 1, topCapHeight: Int(diameter / 2) + 1)
}
public func generateCircleImage(diameter: CGFloat, lineWidth: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))

View file

@ -107,6 +107,12 @@ public class ImmediateTextNode: TextNode {
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated)
}
public func redrawIfPossible() {
if let constrainedSize = self.constrainedSize {
let _ = self.updateLayout(constrainedSize)
}
}
override public func didLoad() {
super.didLoad()
@ -191,6 +197,12 @@ public class ImmediateTextNode: TextNode {
}
public class ASTextNode: ImmediateTextNode {
override public init() {
super.init()
self.maximumNumberOfLines = 0
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return self.updateLayout(constrainedSize)
}

View file

@ -54,15 +54,22 @@ public struct InteractiveTransitionGestureRecognizerDirections: OptionSet {
public static let right: InteractiveTransitionGestureRecognizerDirections = [.rightEdge, .rightCenter]
}
public enum InteractiveTransitionGestureRecognizerEdgeWidth {
case constant(CGFloat)
case widthMultiplier(factor: CGFloat, min: CGFloat, max: CGFloat)
}
public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
private let edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth
private let allowedDirections: (CGPoint) -> InteractiveTransitionGestureRecognizerDirections
private var validatedGesture = false
private var firstLocation: CGPoint = CGPoint()
private var currentAllowedDirections: InteractiveTransitionGestureRecognizerDirections = []
public init(target: Any?, action: Selector?, allowedDirections: @escaping (CGPoint) -> InteractiveTransitionGestureRecognizerDirections) {
public init(target: Any?, action: Selector?, allowedDirections: @escaping (CGPoint) -> InteractiveTransitionGestureRecognizerDirections, edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth = .constant(16.0)) {
self.allowedDirections = allowedDirections
self.edgeWidth = edgeWidth
super.init(target: target, action: action)
@ -120,7 +127,14 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
let absTranslationY: CGFloat = abs(translation.y)
let size = self.view?.bounds.size ?? CGSize()
let edgeWidth: CGFloat = 20.0
let edgeWidth: CGFloat
switch self.edgeWidth {
case let .constant(value):
edgeWidth = value
case let .widthMultiplier(factor, minValue, maxValue):
edgeWidth = max(minValue, min(size.width * factor, maxValue))
}
if !self.validatedGesture {
if self.firstLocation.x < edgeWidth && !self.currentAllowedDirections.contains(.rightEdge) {

View file

@ -990,7 +990,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
}
}
if let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset {
if let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset, topItemFound {
completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset)
bottomItemEdge = max(bottomItemEdge, topItemEdge + completeHeight)
}

View file

@ -16,6 +16,9 @@ open class ASImageNode: ASDisplayNode {
} else {
self.contents = nil
}
if self.image?.size != oldValue?.size {
self.invalidateCalculatedLayout()
}
}
}
@ -24,4 +27,8 @@ open class ASImageNode: ASDisplayNode {
override public init() {
super.init()
}
override public func calculateSizeThatFits(_ contrainedSize: CGSize) -> CGSize {
return self.image?.size ?? CGSize()
}
}

View file

@ -166,6 +166,14 @@ open class TabBarController: ViewController {
return self.tabBarControllerNode.tabBarNode.sourceNodesForController(at: index)
}
public func frameForControllerTab(controller: ViewController) -> CGRect? {
if let index = self.controllers.firstIndex(of: controller) {
return self.tabBarControllerNode.tabBarNode.frameForControllerTab(at: index).flatMap { self.tabBarControllerNode.tabBarNode.view.convert($0, to: self.view) }
} else {
return nil
}
}
override open func loadDisplayNode() {
self.displayNode = TabBarControllerNode(theme: self.theme, navigationBar: self.navigationBar, itemSelected: { [weak self] index, longTap, itemNodes in
if let strongSelf = self {

View file

@ -392,6 +392,11 @@ class TabBarNode: ASDisplayNode {
return [container.imageNode.imageNode, container.imageNode.textImageNode, container.badgeContainerNode]
}
func frameForControllerTab(at index: Int) -> CGRect? {
let container = self.tabBarNodeContainers[index]
return container.imageNode.frame
}
private func reloadTabBarItems() {
for node in self.tabBarNodeContainers {
node.imageNode.removeFromSupernode()

View file

@ -837,10 +837,14 @@ public class TextNode: ASDisplayNode {
} else {
font = defaultFont
}
if let paragraphStyle = attributedString.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle {
resolvedAlignment = paragraphStyle.alignment
if alignment == .center {
resolvedAlignment = .center
} else {
resolvedAlignment = alignment
if let paragraphStyle = attributedString.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle {
resolvedAlignment = paragraphStyle.alignment
} else {
resolvedAlignment = alignment
}
}
} else {
font = defaultFont

View file

@ -51,6 +51,7 @@ public final class HashtagSearchController: TelegramBaseController {
}, peerSelected: { peer in
}, disabledPeerSelected: { _ in
}, togglePeerSelected: { _ in
}, additionalCategorySelected: { _ in
}, messageSelected: { [weak self] peer, message, _ in
if let strongSelf = self {
strongSelf.openMessageFromSearchDisposable.set((storedMessageFromSearchPeer(account: strongSelf.context.account, peer: peer) |> deliverOnMainQueue).start(next: { actualPeerId in

View file

@ -361,9 +361,14 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
addMembersDisposable.set((contactsController.result
|> deliverOnMainQueue
|> castError(AddChannelMemberError.self)
|> mapToSignal { [weak contactsController] contacts -> Signal<Never, AddChannelMemberError> in
|> mapToSignal { [weak contactsController] result -> Signal<Never, AddChannelMemberError> in
contactsController?.displayProgress = true
var contacts: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
contacts = peerIdsValue
}
let signal = context.peerChannelMemberCategoriesContextsManager.addMembers(account: context.account, peerId: peerId, memberIds: contacts.compactMap({ contact -> PeerId? in
switch contact {
case let .peer(contactId):

View file

@ -1257,10 +1257,16 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
let selectionController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .channelCreation, options: []))
(controller.navigationController as? NavigationController)?.replaceAllButRootController(selectionController, animated: true)
let _ = (selectionController.result
|> deliverOnMainQueue).start(next: { [weak selectionController] peerIds in
|> deliverOnMainQueue).start(next: { [weak selectionController] result in
guard let selectionController = selectionController, let navigationController = selectionController.navigationController as? NavigationController else {
return
}
var peerIds: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
peerIds = peerIdsValue
}
let filteredPeerIds = peerIds.compactMap({ peerId -> PeerId? in
if case let .peer(id) = peerId {
return id

View file

@ -1913,7 +1913,12 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
}
if let contactsController = contactsController as? ContactMultiselectionController {
selectAddMemberDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak contactsController] peers in
|> deliverOnMainQueue).start(next: { [weak contactsController] result in
var peers: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
peers = peerIdsValue
}
contactsController?.displayProgress = true
addMemberDisposable.set((addMembers(peers)
|> deliverOnMainQueue).start(error: { error in

View file

@ -175,18 +175,18 @@ final class ChatListIndexTable: Table {
assert(self.updatedPreviousPeerCachedIndices.isEmpty)
}
func commitWithTransaction(postbox: Postbox, alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], updatedPeers: [(Peer?, Peer)], transactionParticipationInTotalUnreadCountUpdates: (added: Set<PeerId>, removed: Set<PeerId>), updatedRootUnreadState: inout ChatListTotalUnreadState?, updatedGroupTotalUnreadSummaries: inout [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], currentUpdatedGroupSummarySynchronizeOperations: inout [PeerGroupAndNamespace: Bool]) {
func commitWithTransaction(postbox: Postbox, alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], updatedPeers: [((Peer, Bool)?, (Peer, Bool))], transactionParticipationInTotalUnreadCountUpdates: (added: Set<PeerId>, removed: Set<PeerId>), updatedRootUnreadState: inout ChatListTotalUnreadState?, updatedGroupTotalUnreadSummaries: inout [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], currentUpdatedGroupSummarySynchronizeOperations: inout [PeerGroupAndNamespace: Bool]) {
var updatedPeerTags: [PeerId: (previous: PeerSummaryCounterTags, updated: PeerSummaryCounterTags)] = [:]
for (previous, updated) in updatedPeers {
let previousTags: PeerSummaryCounterTags
if let previous = previous {
previousTags = postbox.seedConfiguration.peerSummaryCounterTags(previous)
if let (previous, previousIsContact) = previous {
previousTags = postbox.seedConfiguration.peerSummaryCounterTags(previous, previousIsContact)
} else {
previousTags = []
}
let updatedTags = postbox.seedConfiguration.peerSummaryCounterTags(updated)
let updatedTags = postbox.seedConfiguration.peerSummaryCounterTags(updated.0, updated.1)
if previousTags != updatedTags {
updatedPeerTags[updated.id] = (previousTags, updatedTags)
updatedPeerTags[updated.0.id] = (previousTags, updatedTags)
}
}
@ -348,6 +348,7 @@ final class ChatListIndexTable: Table {
guard let peer = postbox.peerTable.get(peerId) else {
continue
}
let isContact = postbox.contactsTable.isContact(peerId: peerId)
let notificationPeerId: PeerId = peer.associatedPeerId ?? peerId
let initialReadState = alteredInitialPeerCombinedReadStates[peerId] ?? postbox.readStateTable.getCombinedState(peerId)
let currentReadState = postbox.readStateTable.getCombinedState(peerId)
@ -435,7 +436,7 @@ final class ChatListIndexTable: Table {
}
if var currentTotalRootUnreadState = totalRootUnreadState {
var keptTags: PeerSummaryCounterTags = postbox.seedConfiguration.peerSummaryCounterTags(peer)
var keptTags: PeerSummaryCounterTags = postbox.seedConfiguration.peerSummaryCounterTags(peer, isContact)
if let (removedTags, addedTags) = updatedPeerTags[peerId] {
keptTags.remove(removedTags)
keptTags.remove(addedTags)
@ -583,6 +584,7 @@ final class ChatListIndexTable: Table {
guard let combinedState = postbox.readStateTable.getCombinedState(peerId) else {
continue
}
let isContact = postbox.contactsTable.isContact(peerId: peerId)
let notificationPeerId: PeerId = peer.associatedPeerId ?? peerId
let notificationSettings = postbox.peerNotificationSettingsTable.getEffective(notificationPeerId)
let inclusion = self.get(peerId: peerId)
@ -590,7 +592,7 @@ final class ChatListIndexTable: Table {
if case .root = groupId {
let peerMessageCount = combinedState.count
let summaryTags = postbox.seedConfiguration.peerSummaryCounterTags(peer)
let summaryTags = postbox.seedConfiguration.peerSummaryCounterTags(peer, isContact)
for tag in summaryTags {
if rootState.absoluteCounters[tag] == nil {
rootState.absoluteCounters[tag] = ChatListTotalUnreadCounters(messageCount: 0, chatCount: 0)

View file

@ -255,7 +255,8 @@ final class ChatListTable: Table {
if let filterPredicate = filterPredicate {
if let peer = postbox.peerTable.get(messageIndex.id.peerId) {
let isUnread = postbox.readStateTable.getCombinedState(messageIndex.id.peerId)?.isUnread ?? false
if filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(messageIndex.id.peerId), isUnread: isUnread) {
let isContact = postbox.contactsTable.isContact(peerId: messageIndex.id.peerId)
if filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(messageIndex.id.peerId), isUnread: isUnread, isContact: isContact, isArchived: groupId != .root) {
passFilter = true
} else {
passFilter = false

View file

@ -88,12 +88,12 @@ public struct ChatListGroupReferenceEntry: Equatable {
}
public enum ChatListEntry: Comparable {
case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo, Bool)
case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo, Bool, Bool)
case HoleEntry(ChatListHole)
public var index: ChatListIndex {
switch self {
case let .MessageEntry(index, _, _, _, _, _, _, _, _):
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _):
return index
case let .HoleEntry(hole):
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
@ -102,9 +102,9 @@ public enum ChatListEntry: Comparable {
public static func ==(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool {
switch lhs {
case let .MessageEntry(lhsIndex, lhsMessage, lhsReadState, lhsSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsInfo, lhsHasFailed):
case let .MessageEntry(lhsIndex, lhsMessage, lhsReadState, lhsSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsInfo, lhsHasFailed, lhsIsContact):
switch rhs {
case let .MessageEntry(rhsIndex, rhsMessage, rhsReadState, rhsSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsInfo, rhsHasFailed):
case let .MessageEntry(rhsIndex, rhsMessage, rhsReadState, rhsSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsInfo, rhsHasFailed, rhsIsContact):
if lhsIndex != rhsIndex {
return false
}
@ -144,6 +144,9 @@ public enum ChatListEntry: Comparable {
if lhsHasFailed != rhsHasFailed {
return false
}
if lhsIsContact != rhsIsContact {
return false
}
return true
default:
return false
@ -181,7 +184,7 @@ private func processedChatListEntry(_ entry: MutableChatListEntry, cachedDataTab
enum MutableChatListEntry: Equatable {
case IntermediateMessageEntry(ChatListIndex, IntermediateMessage?, CombinedPeerReadState?, PeerChatListEmbeddedInterfaceState?)
case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo, Bool)
case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo, Bool, Bool)
case HoleEntry(ChatListHole)
init(_ intermediateEntry: ChatListIntermediateEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) {
@ -197,7 +200,7 @@ enum MutableChatListEntry: Equatable {
switch self {
case let .IntermediateMessageEntry(index, _, _, _):
return index
case let .MessageEntry(index, _, _, _, _, _, _, _, _):
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _):
return index
case let .HoleEntry(hole):
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
@ -297,19 +300,23 @@ private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [Pe
public struct ChatListFilterPredicate {
public var includePeerIds: Set<PeerId>
public var include: (Peer, PeerNotificationSettings?, Bool) -> Bool
public var excludePeerIds: Set<PeerId>
public var include: (Peer, PeerNotificationSettings?, Bool, Bool, Bool) -> Bool
public init(includePeerIds: Set<PeerId>, include: @escaping (Peer, PeerNotificationSettings?, Bool) -> Bool) {
public init(includePeerIds: Set<PeerId>, excludePeerIds: Set<PeerId>, include: @escaping (Peer, PeerNotificationSettings?, Bool, Bool, Bool) -> Bool) {
self.includePeerIds = includePeerIds
self.excludePeerIds = excludePeerIds
self.include = include
}
func includes(peer: Peer, notificationSettings: PeerNotificationSettings?, isUnread: Bool) -> Bool {
func includes(peer: Peer, notificationSettings: PeerNotificationSettings?, isUnread: Bool, isContact: Bool, isArchived: Bool) -> Bool {
if self.excludePeerIds.contains(peer.id) {
return false
}
if self.includePeerIds.contains(peer.id) {
return true
} else {
return self.include(peer, notificationSettings, isUnread)
}
return self.include(peer, notificationSettings, isUnread, isContact, isArchived)
}
}
@ -322,6 +329,7 @@ final class MutableChatListView {
fileprivate var additionalMixedItemIds: Set<PeerId>
fileprivate var additionalMixedPinnedItemIds: Set<PeerId>
fileprivate var additionalMixedItemEntries: [MutableChatListEntry]
fileprivate var additionalMixedPinnedEntries: [MutableChatListEntry]
fileprivate var earlier: MutableChatListEntry?
fileprivate var later: MutableChatListEntry?
fileprivate var entries: [MutableChatListEntry]
@ -340,6 +348,7 @@ final class MutableChatListView {
self.summaryComponents = summaryComponents
self.additionalItemEntries = []
self.additionalMixedItemEntries = []
self.additionalMixedPinnedEntries = []
self.additionalMixedItemIds = Set()
self.additionalMixedPinnedItemIds = Set()
if let filterPredicate = self.filterPredicate {
@ -351,11 +360,16 @@ final class MutableChatListView {
}
}
}
for peerId in self.additionalMixedItemIds.union(self.additionalMixedPinnedItemIds) {
for peerId in self.additionalMixedItemIds {
if let entry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
self.additionalMixedItemEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
}
}
for peerId in self.additionalMixedPinnedItemIds {
if let entry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
self.additionalMixedPinnedEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
}
}
if case .root = groupId, self.filterPredicate == nil {
let itemIds = postbox.additionalChatListItemsTable.get()
self.additionalItemIds = Set(itemIds)
@ -564,8 +578,8 @@ final class MutableChatListView {
for (peerId, settingsChange) in updatedPeerNotificationSettings {
if let peer = postbox.peerTable.get(peerId), !self.additionalMixedItemIds.contains(peerId), !self.additionalMixedPinnedItemIds.contains(peerId) {
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let wasIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.0, isUnread: isUnread)
let isIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.1, isUnread: isUnread)
let wasIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.0, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId), isArchived: false)
let isIncluded = filterPredicate.includes(peer: peer, notificationSettings: settingsChange.1, isUnread: isUnread, isContact: postbox.contactsTable.isContact(peerId: peerId), isArchived: false)
if wasIncluded != isIncluded {
if isIncluded {
let tableEntry: ChatListIntermediateEntry?
@ -588,7 +602,7 @@ final class MutableChatListView {
} else {
loop: for i in 0 ..< self.entries.count {
switch self.entries[i] {
case .MessageEntry(let index, _, _, _, _, _, _, _, _), .IntermediateMessageEntry(let index, _, _, _):
case .MessageEntry(let index, _, _, _, _, _, _, _, _, _), .IntermediateMessageEntry(let index, _, _, _):
if index.messageIndex.id.peerId == peerId {
self.entries.remove(at: i)
hasChanges = true
@ -606,13 +620,13 @@ final class MutableChatListView {
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
var notificationSettingsPeerId = peer.peerId
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
notificationSettingsPeerId = associatedPeerId
}
if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] {
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed)
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
@ -622,13 +636,29 @@ final class MutableChatListView {
for i in 0 ..< self.additionalMixedItemEntries.count {
switch self.additionalMixedItemEntries[i] {
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
var notificationSettingsPeerId = peer.peerId
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
notificationSettingsPeerId = associatedPeerId
}
if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] {
self.additionalMixedItemEntries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed)
self.additionalMixedItemEntries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
continue
}
}
for i in 0 ..< self.additionalMixedPinnedEntries.count {
switch self.additionalMixedPinnedEntries[i] {
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
var notificationSettingsPeerId = peer.peerId
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
notificationSettingsPeerId = associatedPeerId
}
if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] {
self.additionalMixedPinnedEntries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
@ -640,11 +670,11 @@ final class MutableChatListView {
if !transaction.updatedFailedMessagePeerIds.isEmpty {
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, previousHasFailed):
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, previousHasFailed, isContact):
if transaction.updatedFailedMessagePeerIds.contains(index.messageIndex.id.peerId) {
let hasFailed = postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId)
if previousHasFailed != hasFailed {
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed)
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
}
@ -657,14 +687,14 @@ final class MutableChatListView {
if !updatedPeers.isEmpty {
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
var updatedMessage: Message?
if let message = message {
updatedMessage = updateMessagePeers(message, updatedPeers: updatedPeers)
}
let updatedPeer = updatedRenderedPeer(peer, updatedPeers: updatedPeers)
if updatedMessage != nil || updatedPeer != nil {
self.entries[i] = .MessageEntry(index, updatedMessage ?? message, readState, settings, embeddedState, updatedPeer ?? peer, peerPresence, summaryInfo, hasFailed)
self.entries[i] = .MessageEntry(index, updatedMessage ?? message, readState, settings, embeddedState, updatedPeer ?? peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
@ -675,13 +705,13 @@ final class MutableChatListView {
if !updatedPeerPresences.isEmpty {
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, _, summaryInfo, hasFailed):
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, _, summaryInfo, hasFailed, isContact):
var presencePeerId = peer.peerId
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
presencePeerId = associatedPeerId
}
if let presence = updatedPeerPresences[presencePeerId] {
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, presence, summaryInfo, hasFailed)
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, presence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
@ -692,7 +722,7 @@ final class MutableChatListView {
if !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageActionsSummaries.isEmpty {
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, currentSummary, hasFailed):
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, currentSummary, hasFailed, isContact):
var updatedTagSummaryCount: Int32?
var updatedActionsSummaryCount: Int32?
@ -713,7 +743,7 @@ final class MutableChatListView {
if updatedTagSummaryCount != nil || updatedActionsSummaryCount != nil {
let summaryInfo = ChatListMessageTagSummaryInfo(tagSummaryCount: updatedTagSummaryCount ?? currentSummary.tagSummaryCount, actionsSummaryCount: updatedActionsSummaryCount ?? currentSummary.actionsSummaryCount)
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed)
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact)
hasChanges = true
}
default:
@ -760,11 +790,18 @@ final class MutableChatListView {
}
if updateAdditionalMixedItems {
self.additionalMixedItemEntries.removeAll()
for peerId in self.additionalMixedItemIds.union(self.additionalMixedPinnedItemIds) {
for peerId in self.additionalMixedItemIds {
if let entry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
self.additionalMixedItemEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
}
}
self.additionalMixedPinnedEntries.removeAll()
for peerId in self.additionalMixedPinnedItemIds {
if let entry = postbox.chatListTable.getEntry(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) {
self.additionalMixedPinnedEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
}
}
hasChanges = true
}
return hasChanges
@ -773,10 +810,11 @@ final class MutableChatListView {
func add(_ initialEntry: MutableChatListEntry, postbox: Postbox) -> Bool {
if let filterPredicate = self.filterPredicate {
switch initialEntry {
case .IntermediateMessageEntry(let index, _, _, _), .MessageEntry(let index, _, _, _, _, _, _, _, _):
case .IntermediateMessageEntry(let index, _, _, _), .MessageEntry(let index, _, _, _, _, _, _, _, _, _):
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
let isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
if !filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread: isUnread) {
let isContact = postbox.contactsTable.isContact(peerId: peer.id)
if !filterPredicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread: isUnread, isContact: isContact, isArchived: false) {
return false
}
if self.additionalMixedItemIds.contains(peer.id) {
@ -918,64 +956,34 @@ final class MutableChatListView {
func complete(postbox: Postbox, context: MutableChatListViewReplayContext) {
if context.removedEntries {
var addedEntries: [MutableChatListEntry] = []
var latestAnchor: ChatListIndex?
if let last = self.entries.last {
latestAnchor = last.index
var index = ChatListIndex.absoluteUpperBound
if !self.entries.isEmpty && self.later != nil {
index = self.entries[self.entries.count / 2].index
}
if latestAnchor == nil {
if let later = self.later {
latestAnchor = later.index
let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.count, filterPredicate: self.filterPredicate)
var previousEntryByPeerId: [PeerId: MutableChatListEntry] = [:]
for entry in self.entries {
switch entry {
case let .MessageEntry(messageEntry):
previousEntryByPeerId[messageEntry.0.messageIndex.id.peerId] = entry
default:
break
}
}
if let later = self.later {
addedEntries += postbox.fetchLaterChatEntries(groupId: self.groupId, index: later.index.predecessor, count: self.count, filterPredicate: filterPredicate)
}
if let earlier = self.earlier {
addedEntries += postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlier.index.successor, count: self.count, filterPredicate: filterPredicate)
}
addedEntries += self.entries
addedEntries.sort(by: { $0.index < $1.index })
var i = addedEntries.count - 1
while i >= 1 {
if addedEntries[i].index.messageIndex.id == addedEntries[i - 1].index.messageIndex.id {
addedEntries.remove(at: i)
}
i -= 1
}
self.entries = []
var anchorIndex = addedEntries.count - 1
if let latestAnchor = latestAnchor {
var i = addedEntries.count - 1
while i >= 0 {
if addedEntries[i].index <= latestAnchor {
anchorIndex = i
break
self.entries = entries
for i in 0 ..< self.entries.count {
switch self.entries[i] {
case let .MessageEntry(messageEntry):
if let previousEntry = previousEntryByPeerId[messageEntry.0.messageIndex.id.peerId] {
self.entries[i] = previousEntry
}
i -= 1
default:
break
}
}
self.later = nil
if anchorIndex + 1 < addedEntries.count {
self.later = addedEntries[anchorIndex + 1]
}
i = anchorIndex
while i >= 0 && i > anchorIndex - self.count {
self.entries.insert(addedEntries[i], at: 0)
i -= 1
}
self.earlier = nil
if anchorIndex - self.count >= 0 {
self.earlier = addedEntries[anchorIndex - self.count]
}
self.earlier = earlier
self.later = later
} else {
if context.invalidEarlier {
var earlyId: ChatListIndex?
@ -1023,6 +1031,7 @@ final class MutableChatListView {
var peers = SimpleDictionary<PeerId, Peer>()
var notificationSettings: PeerNotificationSettings?
var presence: PeerPresence?
var isContact: Bool = false
if let peer = getPeer(index.messageIndex.id.peerId) {
peers[peer.id] = peer
if let associatedPeerId = peer.associatedPeerId {
@ -1031,9 +1040,11 @@ final class MutableChatListView {
}
notificationSettings = getPeerNotificationSettings(associatedPeerId)
presence = getPeerPresence(associatedPeerId)
isContact = postbox.contactsTable.isContact(peerId: associatedPeerId)
} else {
notificationSettings = getPeerNotificationSettings(index.messageIndex.id.peerId)
presence = getPeerPresence(index.messageIndex.id.peerId)
isContact = postbox.contactsTable.isContact(peerId: peer.id)
}
}
@ -1052,7 +1063,7 @@ final class MutableChatListView {
actionsSummaryCount = postbox.pendingMessageActionsMetadataTable.getCount(.peerNamespaceAction(key.peerId, key.namespace, key.type))
}
return .MessageEntry(index, renderedMessage, combinedReadState, notificationSettings, embeddedState, RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence, ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount), postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId))
return .MessageEntry(index, renderedMessage, combinedReadState, notificationSettings, embeddedState, RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence, ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount), postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact)
default:
return nil
}
@ -1074,6 +1085,11 @@ final class MutableChatListView {
self.additionalMixedItemEntries[i] = updatedEntry
}
}
for i in 0 ..< self.additionalMixedPinnedEntries.count {
if let updatedEntry = self.renderEntry(self.additionalMixedPinnedEntries[i], postbox: postbox, renderMessage: renderMessage, getPeer: getPeer, getPeerNotificationSettings: getPeerNotificationSettings, getPeerPresence: getPeerPresence) {
self.additionalMixedPinnedEntries[i] = updatedEntry
}
}
}
}
@ -1091,30 +1107,49 @@ public final class ChatListView {
var entries: [ChatListEntry] = []
for entry in mutableView.entries {
switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
entries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed))
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
entries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact))
case let .HoleEntry(hole):
entries.append(.HoleEntry(hole))
case .IntermediateMessageEntry:
assertionFailure()
}
}
if !mutableView.additionalMixedItemEntries.isEmpty {
if !mutableView.additionalMixedItemEntries.isEmpty || !mutableView.additionalMixedPinnedEntries.isEmpty {
var existingIds = Set<PeerId>()
for entry in entries {
if case let .MessageEntry(messageEntry) = entry {
existingIds.insert(messageEntry.0.messageIndex.id.peerId)
}
}
loop: for entry in mutableView.additionalMixedItemEntries {
for entry in mutableView.additionalMixedItemEntries {
if case let .MessageEntry(messageEntry) = entry {
if !existingIds.contains(messageEntry.0.messageIndex.id.peerId) {
switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
if let filterPredicate = mutableView.filterPredicate, let peerValue = peer.peer {
if filterPredicate.includes(peer: peerValue, notificationSettings: notificationSettings, isUnread: combinedReadState?.isUnread ?? false) {
if filterPredicate.includes(peer: peerValue, notificationSettings: notificationSettings, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, isArchived: true) {
existingIds.insert(messageEntry.0.messageIndex.id.peerId)
entries.append(.MessageEntry(ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex), message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed))
entries.append(.MessageEntry(ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex), message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact))
}
}
case let .HoleEntry(hole):
entries.append(.HoleEntry(hole))
case .IntermediateMessageEntry:
assertionFailure()
}
}
}
}
for entry in mutableView.additionalMixedPinnedEntries {
if case let .MessageEntry(messageEntry) = entry {
if !existingIds.contains(messageEntry.0.messageIndex.id.peerId) {
switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
if let filterPredicate = mutableView.filterPredicate, let peerValue = peer.peer {
if filterPredicate.includes(peer: peerValue, notificationSettings: notificationSettings, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, isArchived: false) {
existingIds.insert(messageEntry.0.messageIndex.id.peerId)
entries.append(.MessageEntry(ChatListIndex(pinningIndex: nil, messageIndex: index.messageIndex), message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact))
}
}
case let .HoleEntry(hole):
@ -1135,8 +1170,8 @@ public final class ChatListView {
var additionalItemEntries: [ChatListEntry] = []
for entry in mutableView.additionalItemEntries {
switch entry {
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed):
additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed))
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact):
additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact))
case .HoleEntry:
assertionFailure()
case .IntermediateMessageEntry:

View file

@ -60,6 +60,21 @@ final class ContactTable: Table {
self.peerIds = nil
}
func transactionUpdatedPeers() -> [PeerId: (Bool, Bool)] {
var result: [PeerId: (Bool, Bool)] = [:]
if let peerIdsBeforeModification = self.peerIdsBeforeModification {
if let peerIds = self.peerIds {
let removedPeerIds = peerIdsBeforeModification.subtracting(peerIds)
let addedPeerIds = peerIds.subtracting(peerIdsBeforeModification)
for peerId in removedPeerIds.union(addedPeerIds) {
result[peerId] = (removedPeerIds.contains(peerId), addedPeerIds.contains(peerId))
}
}
}
return result
}
override func beforeCommit() {
if let peerIdsBeforeModification = self.peerIdsBeforeModification {
if let peerIds = self.peerIds {

View file

@ -4,11 +4,13 @@ final class MutableBasicPeerView: MutablePostboxView {
private let peerId: PeerId
fileprivate var peer: Peer?
fileprivate var notificationSettings: PeerNotificationSettings?
fileprivate var isContact: Bool
init(postbox: Postbox, peerId: PeerId) {
self.peerId = peerId
self.peer = postbox.peerTable.get(peerId)
self.notificationSettings = postbox.peerNotificationSettingsTable.getEffective(peerId)
self.isContact = postbox.contactsTable.isContact(peerId: self.peerId)
}
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
@ -21,6 +23,13 @@ final class MutableBasicPeerView: MutablePostboxView {
self.notificationSettings = postbox.peerNotificationSettingsTable.getEffective(peerId)
updated = true
}
if transaction.replaceContactPeerIds != nil {
let isContact = postbox.contactsTable.isContact(peerId: self.peerId)
if isContact != self.isContact {
self.isContact = isContact
updated = true
}
}
return updated
}
@ -33,9 +42,11 @@ final class MutableBasicPeerView: MutablePostboxView {
public final class BasicPeerView: PostboxView {
public let peer: Peer?
public let notificationSettings: PeerNotificationSettings?
public let isContact: Bool
init(_ view: MutableBasicPeerView) {
self.peer = view.peer
self.notificationSettings = view.notificationSettings
self.isContact = view.isContact
}
}

View file

@ -50,11 +50,12 @@ final class PeerTable: Table {
assert(self.updatedInitialPeers.isEmpty)
}
func transactionUpdatedPeers() -> [(Peer?, Peer)] {
var result: [(Peer?, Peer)] = []
func transactionUpdatedPeers(contactsTable: ContactTable) -> [((Peer, Bool)?, (Peer, Bool))] {
var result: [((Peer, Bool)?, (Peer, Bool))] = []
for (peerId, initialPeer) in self.updatedInitialPeers {
if let peer = self.get(peerId) {
result.append((initialPeer, peer))
let isContact = contactsTable.isContact(peerId: peerId)
result.append((initialPeer.flatMap { ($0, isContact) }, (peer, isContact)))
} else {
assertionFailure()
}

View file

@ -776,7 +776,23 @@ public final class Transaction {
if let peer = postbox.peerTable.get(peerId) {
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let notificationsPeerId = peer.notificationSettingsPeerId ?? peerId
if predicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread) {
let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId)
if predicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact, isArchived: false) {
includedPeerIds[peer.id] = true
return true
} else {
return false
}
} else {
return false
}
})
let archivedCount = postbox.chatListTable.countWithPredicate(groupId: .group(1), predicate: { peerId in
if let peer = postbox.peerTable.get(peerId) {
let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
let notificationsPeerId = peer.notificationSettingsPeerId ?? peerId
let isContact = postbox.contactsTable.isContact(peerId: notificationsPeerId)
if predicate.includes(peer: peer, notificationSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact, isArchived: false) {
includedPeerIds[peer.id] = true
return true
} else {
@ -793,7 +809,7 @@ public final class Transaction {
}
}
}
return count
return count + archivedCount
}
public func legacyGetAccessChallengeData() -> PostboxAccessChallengeData {
@ -1400,9 +1416,9 @@ public final class Postbox {
self.messageHistoryMetadataTable.setShouldReindexUnreadCounts(value: true)
self.messageHistoryMetadataTable.setShouldReindexUnreadCountsState(value: reindexUnreadVersion)
}
#if DEBUG
//#if DEBUG
self.messageHistoryMetadataTable.setShouldReindexUnreadCounts(value: true)
#endif
//#endif
if self.messageHistoryMetadataTable.shouldReindexUnreadCounts() {
self.groupMessageStatsTable.removeAll()
@ -1686,7 +1702,7 @@ public final class Postbox {
self.synchronizeGroupMessageStatsTable.set(groupId: groupId, namespace: namespace, needsValidation: false, operations: &self.currentUpdatedGroupSummarySynchronizeOperations)
}
private func mappedChatListFilterPredicate(_ predicate: ChatListFilterPredicate) -> (ChatListIntermediateEntry) -> Bool {
private func mappedChatListFilterPredicate(_ predicate: ChatListFilterPredicate, isArchived: Bool) -> (ChatListIntermediateEntry) -> Bool {
return { entry in
switch entry {
case let .message(index, _, _):
@ -1696,7 +1712,8 @@ public final class Postbox {
if let peer = self.peerTable.get(index.messageIndex.id.peerId) {
let isUnread = self.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
if predicate.includes(peer: peer, notificationSettings: self.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread) {
let isContact = self.contactsTable.isContact(peerId: notificationsPeerId)
if predicate.includes(peer: peer, notificationSettings: self.peerNotificationSettingsTable.getEffective(notificationsPeerId), isUnread: isUnread, isContact: isContact, isArchived: isArchived) {
return true
} else {
return false
@ -1711,7 +1728,9 @@ public final class Postbox {
}
func fetchAroundChatEntries(groupId: PeerGroupId, index: ChatListIndex, count: Int, filterPredicate: ChatListFilterPredicate?) -> (entries: [MutableChatListEntry], earlier: MutableChatListEntry?, later: MutableChatListEntry?) {
let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate)
let mappedPredicate = filterPredicate.flatMap { predicate in
self.mappedChatListFilterPredicate(predicate, isArchived: groupId != .root)
}
let (intermediateEntries, intermediateLower, intermediateUpper) = self.chatListTable.entriesAround(groupId: groupId, index: index, messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
@ -1727,7 +1746,9 @@ public final class Postbox {
}
func fetchEarlierChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ChatListFilterPredicate?) -> [MutableChatListEntry] {
let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate)
let mappedPredicate = filterPredicate.flatMap { predicate in
self.mappedChatListFilterPredicate(predicate, isArchived: groupId != .root)
}
let intermediateEntries = self.chatListTable.earlierEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
@ -1736,7 +1757,9 @@ public final class Postbox {
}
func fetchLaterChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ChatListFilterPredicate?) -> [MutableChatListEntry] {
let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate)
let mappedPredicate = filterPredicate.flatMap { predicate in
self.mappedChatListFilterPredicate(predicate, isArchived: groupId != .root)
}
let intermediateEntries = self.chatListTable.laterEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate)
let entries: [MutableChatListEntry] = intermediateEntries.map { entry in
return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable)
@ -1775,7 +1798,26 @@ public final class Postbox {
self.peerChatTopTaggedMessageIdsTable.replay(historyOperationsByPeerId: self.currentOperationsByPeerId)
let alteredInitialPeerCombinedReadStates = self.readStateTable.transactionAlteredInitialPeerCombinedReadStates()
let updatedPeers = self.peerTable.transactionUpdatedPeers()
var updatedPeers = self.peerTable.transactionUpdatedPeers(contactsTable: self.contactsTable)
let updatedContacts = self.contactsTable.transactionUpdatedPeers()
if !updatedContacts.isEmpty {
var updatedPeerIdToIndex: [PeerId: Int] = [:]
var index = 0
for (_, peer) in updatedPeers {
updatedPeerIdToIndex[peer.0.id] = index
}
index += 1
for (peerId, change) in updatedContacts {
if let index = updatedPeerIdToIndex[peerId] {
if let (peer, _) = updatedPeers[index].0 {
updatedPeers[index].0 = (peer, change.0)
}
updatedPeers[index].1 = (updatedPeers[index].1.0, change.1)
} else if let peer = self.peerTable.get(peerId) {
updatedPeers.append(((peer, change.0), (peer, change.1)))
}
}
}
let transactionParticipationInTotalUnreadCountUpdates = self.peerNotificationSettingsTable.transactionParticipationInTotalUnreadCountUpdates(postbox: self)
self.chatListIndexTable.commitWithTransaction(postbox: self, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, updatedPeers: updatedPeers, transactionParticipationInTotalUnreadCountUpdates: transactionParticipationInTotalUnreadCountUpdates, updatedRootUnreadState: &self.currentUpdatedTotalUnreadState, updatedGroupTotalUnreadSummaries: &self.currentUpdatedGroupTotalUnreadSummaries, currentUpdatedGroupSummarySynchronizeOperations: &self.currentUpdatedGroupSummarySynchronizeOperations)
@ -3268,7 +3310,7 @@ public final class Postbox {
fileprivate func reindexUnreadCounters() {
self.groupMessageStatsTable.removeAll()
let startTime = CFAbsoluteTimeGetCurrent()
let _ = CFAbsoluteTimeGetCurrent()
let (rootState, summaries) = self.chatListIndexTable.debugReindexUnreadCounts(postbox: self)
self.messageHistoryMetadataTable.setChatListTotalUnreadState(rootState)

View file

@ -17,13 +17,13 @@ public final class SeedConfiguration {
public let messageTagsWithSummary: MessageTags
public let existingGlobalMessageTags: GlobalMessageTags
public let peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace]
public let peerSummaryCounterTags: (Peer) -> PeerSummaryCounterTags
public let peerSummaryCounterTags: (Peer, Bool) -> PeerSummaryCounterTags
public let additionalChatListIndexNamespace: MessageId.Namespace?
public let messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>
public let defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState]
public let chatMessagesNamespaces: Set<MessageId.Namespace>
public init(globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState], chatMessagesNamespaces: Set<MessageId.Namespace>) {
public init(globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer, Bool) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState], chatMessagesNamespaces: Set<MessageId.Namespace>) {
self.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces
self.initializeChatListWithHole = initializeChatListWithHole
self.messageHoles = messageHoles

View file

@ -69,7 +69,7 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode {
let contentHeight: CGFloat = largeScreen ? 420.0 : 400.0
let iconSize = self.iconNode.measure(CGSize(width: 400.0, height: 400.0))
let labelSize = self.labelNode.measure(CGSize(width: 295.0, height: CGFloat.greatestFiniteMagnitude))
let labelSize = self.labelNode.updateLayout(CGSize(width: 295.0, height: CGFloat.greatestFiniteMagnitude))
let buttonSize = self.buttonNode.measure(CGSize(width: 295.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: insets.top + floor((availableHeight - contentHeight) / 2.0) + floor(iconSize.height * (largeScreen ? CGFloat(0.2) : CGFloat(0.5)))), size: iconSize))

View file

@ -26,13 +26,13 @@ private struct CounterTagSettings: OptionSet {
init(summaryTags: PeerSummaryCounterTags) {
var result = CounterTagSettings()
if summaryTags.contains(.privateChat) {
if summaryTags.contains(.contact) {
result.insert(.regularChatsAndPrivateGroups)
}
if summaryTags.contains(.channel) {
result.insert(.channels)
}
if summaryTags.contains(.publicGroup) {
if summaryTags.contains(.largeGroup) {
result.insert(.publicGroups)
}
self = result
@ -41,13 +41,13 @@ private struct CounterTagSettings: OptionSet {
func toSumaryTags() -> PeerSummaryCounterTags {
var result = PeerSummaryCounterTags()
if self.contains(.regularChatsAndPrivateGroups) {
result.insert(.privateChat)
result.insert(.secretChat)
result.insert(.contact)
result.insert(.nonContact)
result.insert(.bot)
result.insert(.privateGroup)
result.insert(.smallGroup)
}
if self.contains(.publicGroups) {
result.insert(.publicGroup)
result.insert(.largeGroup)
}
if self.contains(.channels) {
result.insert(.channel)

View file

@ -770,7 +770,12 @@ func selectivePrivacySettingsController(context: AccountContext, kind: Selective
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: false), options: []))
addPeerDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] peerIds in
|> deliverOnMainQueue).start(next: { [weak controller] result in
var peerIds: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
peerIds = peerIdsValue
}
if peerIds.isEmpty {
controller?.dismiss()
return

View file

@ -293,7 +293,12 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: false), options: []))
addPeerDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] peerIds in
|> deliverOnMainQueue).start(next: { [weak controller] result in
var peerIds: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
peerIds = peerIdsValue
}
let applyPeers: Signal<Void, NoError> = peersPromise.get()
|> take(1)
|> mapToSignal { peers -> Signal<[SelectivePrivacyPeer], NoError> in

View file

@ -707,7 +707,7 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
}
if enableFilters {
//TODO:localize
entries.append(.filters(presentationData.theme, UIImage(bundleImageName: "Settings/MenuIcons/SavedMessages")?.precomposed(), "Chat Filters", ""))
entries.append(.filters(presentationData.theme, UIImage(bundleImageName: "Settings/MenuIcons/ChatListFilters")?.precomposed(), "Chat Filters", ""))
}
let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed)

View file

@ -213,7 +213,8 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
var items: [ChatListItem] = []
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in
gesture?.cancel()
}, present: { _ in })
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)

View file

@ -766,7 +766,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
var items: [ChatListItem] = []
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in
gesture?.cancel()
}, present: { _ in
})

View file

@ -350,7 +350,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
var items: [ChatListItem] = []
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in
gesture?.cancel()
}, present: { _ in
})

View file

@ -813,7 +813,7 @@ public final class ShareController: ViewController {
var peers: [RenderedPeer] = []
for entry in view.0.entries.reversed() {
switch entry {
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _):
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _):
if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id, canSendMessagesToPeer(peer) {
peers.append(renderedPeer)
}

View file

@ -172,12 +172,21 @@ public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable {
}
public extension PeerSummaryCounterTags {
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)
static let contact = PeerSummaryCounterTags(rawValue: 1 << 3)
static let nonContact = PeerSummaryCounterTags(rawValue: 1 << 4)
static let smallGroup = PeerSummaryCounterTags(rawValue: 1 << 5)
static let largeGroup = PeerSummaryCounterTags(rawValue: 1 << 6)
static let bot = PeerSummaryCounterTags(rawValue: 1 << 7)
static let channel = PeerSummaryCounterTags(rawValue: 1 << 8)
static let all: PeerSummaryCounterTags = [
.contact,
.nonContact,
.smallGroup,
.largeGroup,
.bot,
.channel
]
}
private enum PreferencesKeyValues: Int32 {

View file

@ -18,31 +18,33 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
globalMessageIdsPeerIdNamespaces.insert(GlobalMessageIdsNamespace(peerIdNamespace: peerIdNamespace, messageIdNamespace: Namespaces.Message.Cloud))
}
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
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, isContact in
if let peer = peer as? TelegramUser {
if peer.botInfo != nil {
return .bot
} else if isContact {
return .contact
} else {
return .privateChat
return .nonContact
}
} else if let _ = peer as? TelegramGroup {
return .privateGroup
return .smallGroup
} else if let _ = peer as? TelegramSecretChat {
return .secretChat
return .nonContact
} else if let channel = peer as? TelegramChannel {
switch channel.info {
case .broadcast:
return .channel
case .group:
if channel.username != nil {
return .publicGroup
return .largeGroup
} else {
return .privateGroup
return .smallGroup
}
}
} else {
assertionFailure()
return .privateChat
return .nonContact
}
}, 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

@ -338,6 +338,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-353862078] = { return Api.contacts.Contacts.parse_contacts($0) }
dict[-1798033689] = { return Api.ChannelMessagesFilter.parse_channelMessagesFilterEmpty($0) }
dict[-847783593] = { return Api.ChannelMessagesFilter.parse_channelMessagesFilter($0) }
dict[2004110666] = { return Api.DialogFilterSuggested.parse_dialogFilterSuggested($0) }
dict[326715557] = { return Api.auth.PasswordRecovery.parse_passwordRecovery($0) }
dict[-1803769784] = { return Api.messages.BotResults.parse_botResults($0) }
dict[1928391342] = { return Api.InputDocument.parse_inputDocumentEmpty($0) }
@ -604,7 +605,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1551583367] = { return Api.ReceivedNotifyMessage.parse_receivedNotifyMessage($0) }
dict[-57668565] = { return Api.ChatParticipants.parse_chatParticipantsForbidden($0) }
dict[1061556205] = { return Api.ChatParticipants.parse_chatParticipants($0) }
dict[351868460] = { return Api.DialogFilter.parse_dialogFilter($0) }
dict[1687327098] = { return Api.DialogFilter.parse_dialogFilter($0) }
dict[-1056001329] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentialsSaved($0) }
dict[873977640] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentials($0) }
dict[178373535] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentialsApplePay($0) }
@ -1052,6 +1053,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.ChannelMessagesFilter:
_1.serialize(buffer, boxed)
case let _1 as Api.DialogFilterSuggested:
_1.serialize(buffer, boxed)
case let _1 as Api.auth.PasswordRecovery:
_1.serialize(buffer, boxed)
case let _1 as Api.messages.BotResults:

View file

@ -10286,6 +10286,46 @@ public extension Api {
}
}
}
public enum DialogFilterSuggested: TypeConstructorDescription {
case dialogFilterSuggested(filter: Api.DialogFilter, description: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .dialogFilterSuggested(let filter, let description):
if boxed {
buffer.appendInt32(2004110666)
}
filter.serialize(buffer, true)
serializeString(description, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .dialogFilterSuggested(let filter, let description):
return ("dialogFilterSuggested", [("filter", filter), ("description", description)])
}
}
public static func parse_dialogFilterSuggested(_ reader: BufferReader) -> DialogFilterSuggested? {
var _1: Api.DialogFilter?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.DialogFilter
}
var _2: String?
_2 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.DialogFilterSuggested.dialogFilterSuggested(filter: _1!, description: _2!)
}
else {
return nil
}
}
}
public enum InputDocument: TypeConstructorDescription {
case inputDocumentEmpty
@ -17220,13 +17260,13 @@ public extension Api {
}
public enum DialogFilter: TypeConstructorDescription {
case dialogFilter(flags: Int32, id: Int32, title: String, includePeers: [Api.InputPeer])
case dialogFilter(flags: Int32, id: Int32, title: String, includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .dialogFilter(let flags, let id, let title, let includePeers):
case .dialogFilter(let flags, let id, let title, let includePeers, let excludePeers):
if boxed {
buffer.appendInt32(351868460)
buffer.appendInt32(1687327098)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(id, buffer: buffer, boxed: false)
@ -17236,14 +17276,19 @@ public extension Api {
for item in includePeers {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(excludePeers.count))
for item in excludePeers {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .dialogFilter(let flags, let id, let title, let includePeers):
return ("dialogFilter", [("flags", flags), ("id", id), ("title", title), ("includePeers", includePeers)])
case .dialogFilter(let flags, let id, let title, let includePeers, let excludePeers):
return ("dialogFilter", [("flags", flags), ("id", id), ("title", title), ("includePeers", includePeers), ("excludePeers", excludePeers)])
}
}
@ -17258,12 +17303,17 @@ public extension Api {
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self)
}
var _5: [Api.InputPeer]?
if let _ = reader.readInt32() {
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.DialogFilter.dialogFilter(flags: _1!, id: _2!, title: _3!, includePeers: _4!)
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.DialogFilter.dialogFilter(flags: _1!, id: _2!, title: _3!, includePeers: _4!, excludePeers: _5!)
}
else {
return nil

View file

@ -3229,6 +3229,20 @@ public extension Api {
})
}
public static func getSuggestedDialogFilters() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.DialogFilterSuggested]>) {
let buffer = Buffer()
buffer.appendInt32(-1566780372)
return (FunctionDescription(name: "messages.getSuggestedDialogFilters", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.DialogFilterSuggested]? in
let reader = BufferReader(buffer)
var result: [Api.DialogFilterSuggested]?
if let _ = reader.readInt32() {
result = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogFilterSuggested.self)
}
return result
})
}
public static func updateDialogFilter(flags: Int32, id: Int32, filter: Api.DialogFilter?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(450142282)

View file

@ -336,7 +336,7 @@ final class ChatHistoryPreloadManager {
var indices: [(ChatHistoryPreloadIndex, Bool, Bool)] = []
for entry in view.0.entries {
if case let .MessageEntry(index, _, readState, notificationSettings, _, _, _, _, _) = entry {
if case let .MessageEntry(index, _, readState, notificationSettings, _, _, _, _, _, _) = entry {
var hasUnread = false
if let readState = readState {
hasUnread = readState.count != 0

View file

@ -24,20 +24,20 @@ public struct ChatListFilterPeerCategories: OptionSet, Hashable {
self.rawValue = rawValue
}
public static let privateChats = ChatListFilterPeerCategories(rawValue: 1 << 0)
public static let secretChats = ChatListFilterPeerCategories(rawValue: 1 << 1)
public static let privateGroups = ChatListFilterPeerCategories(rawValue: 1 << 2)
public static let bots = ChatListFilterPeerCategories(rawValue: 1 << 3)
public static let publicGroups = ChatListFilterPeerCategories(rawValue: 1 << 4)
public static let channels = ChatListFilterPeerCategories(rawValue: 1 << 5)
public static let contacts = ChatListFilterPeerCategories(rawValue: 1 << 0)
public static let nonContacts = ChatListFilterPeerCategories(rawValue: 1 << 1)
public static let smallGroups = ChatListFilterPeerCategories(rawValue: 1 << 2)
public static let largeGroups = ChatListFilterPeerCategories(rawValue: 1 << 3)
public static let channels = ChatListFilterPeerCategories(rawValue: 1 << 4)
public static let bots = ChatListFilterPeerCategories(rawValue: 1 << 5)
public static let all: ChatListFilterPeerCategories = [
.privateChats,
.secretChats,
.privateGroups,
.bots,
.publicGroups,
.channels
.contacts,
.nonContacts,
.smallGroups,
.largeGroups,
.channels,
.bots
]
}
@ -48,10 +48,10 @@ private struct ChatListFilterPeerApiCategories: OptionSet {
self.rawValue = rawValue
}
static let privateChats = ChatListFilterPeerApiCategories(rawValue: 1 << 0)
static let secretChats = ChatListFilterPeerApiCategories(rawValue: 1 << 1)
static let privateGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 2)
static let publicGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 3)
static let contacts = ChatListFilterPeerApiCategories(rawValue: 1 << 0)
static let nonContacts = ChatListFilterPeerApiCategories(rawValue: 1 << 1)
static let smallGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 2)
static let largeGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 3)
static let channels = ChatListFilterPeerApiCategories(rawValue: 1 << 4)
static let bots = ChatListFilterPeerApiCategories(rawValue: 1 << 5)
}
@ -60,17 +60,17 @@ extension ChatListFilterPeerCategories {
init(apiFlags: Int32) {
let flags = ChatListFilterPeerApiCategories(rawValue: apiFlags)
var result: ChatListFilterPeerCategories = []
if flags.contains(.privateChats) {
result.insert(.privateChats)
if flags.contains(.contacts) {
result.insert(.contacts)
}
if flags.contains(.secretChats) {
result.insert(.secretChats)
if flags.contains(.nonContacts) {
result.insert(.nonContacts)
}
if flags.contains(.privateGroups) {
result.insert(.privateGroups)
if flags.contains(.smallGroups) {
result.insert(.smallGroups)
}
if flags.contains(.publicGroups) {
result.insert(.publicGroups)
if flags.contains(.largeGroups) {
result.insert(.largeGroups)
}
if flags.contains(.channels) {
result.insert(.channels)
@ -83,17 +83,17 @@ extension ChatListFilterPeerCategories {
var apiFlags: Int32 {
var result: ChatListFilterPeerApiCategories = []
if self.contains(.privateChats) {
result.insert(.privateChats)
if self.contains(.contacts) {
result.insert(.contacts)
}
if self.contains(.secretChats) {
result.insert(.secretChats)
if self.contains(.nonContacts) {
result.insert(.nonContacts)
}
if self.contains(.privateGroups) {
result.insert(.privateGroups)
if self.contains(.smallGroups) {
result.insert(.smallGroups)
}
if self.contains(.publicGroups) {
result.insert(.publicGroups)
if self.contains(.largeGroups) {
result.insert(.largeGroups)
}
if self.contains(.channels) {
result.insert(.channels)
@ -109,18 +109,24 @@ public struct ChatListFilterData: Equatable, Hashable {
public var categories: ChatListFilterPeerCategories
public var excludeMuted: Bool
public var excludeRead: Bool
public var excludeArchived: Bool
public var includePeers: [PeerId]
public var excludePeers: [PeerId]
public init(
categories: ChatListFilterPeerCategories,
excludeMuted: Bool,
excludeRead: Bool,
includePeers: [PeerId]
excludeArchived: Bool,
includePeers: [PeerId],
excludePeers: [PeerId]
) {
self.categories = categories
self.excludeMuted = excludeMuted
self.excludeRead = excludeRead
self.excludeArchived = excludeArchived
self.includePeers = includePeers
self.excludePeers = excludePeers
}
}
@ -146,7 +152,9 @@ public struct ChatListFilter: PostboxCoding, Equatable {
categories: ChatListFilterPeerCategories(rawValue: decoder.decodeInt32ForKey("categories", orElse: 0)),
excludeMuted: decoder.decodeInt32ForKey("excludeMuted", orElse: 0) != 0,
excludeRead: decoder.decodeInt32ForKey("excludeRead", orElse: 0) != 0,
includePeers: decoder.decodeInt64ArrayForKey("includePeers").map(PeerId.init)
excludeArchived: decoder.decodeInt32ForKey("excludeArchived", orElse: 0) != 0,
includePeers: decoder.decodeInt64ArrayForKey("includePeers").map(PeerId.init),
excludePeers: decoder.decodeInt64ArrayForKey("excludePeers").map(PeerId.init)
)
}
@ -156,14 +164,16 @@ public struct ChatListFilter: PostboxCoding, Equatable {
encoder.encodeInt32(self.data.categories.rawValue, forKey: "categories")
encoder.encodeInt32(self.data.excludeMuted ? 1 : 0, forKey: "excludeMuted")
encoder.encodeInt32(self.data.excludeRead ? 1 : 0, forKey: "excludeRead")
encoder.encodeInt32(self.data.excludeArchived ? 1 : 0, forKey: "excludeArchived")
encoder.encodeInt64Array(self.data.includePeers.map { $0.toInt64() }, forKey: "includePeers")
encoder.encodeInt64Array(self.data.excludePeers.map { $0.toInt64() }, forKey: "excludePeers")
}
}
extension ChatListFilter {
init(apiFilter: Api.DialogFilter) {
switch apiFilter {
case let .dialogFilter(flags, id, title, includePeers):
case let .dialogFilter(flags, id, title, includePeers, excludePeers):
self.init(
id: id,
title: title,
@ -171,6 +181,7 @@ extension ChatListFilter {
categories: ChatListFilterPeerCategories(apiFlags: flags),
excludeMuted: (flags & (1 << 11)) != 0,
excludeRead: (flags & (1 << 12)) != 0,
excludeArchived: false,
includePeers: includePeers.compactMap { peer -> PeerId? in
switch peer {
case let .inputPeerUser(userId, _):
@ -182,6 +193,18 @@ extension ChatListFilter {
default:
return nil
}
},
excludePeers: excludePeers.compactMap { peer -> PeerId? in
switch peer {
case let .inputPeerUser(userId, _):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
case let .inputPeerChat(chatId):
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)
case let .inputPeerChannel(channelId, _):
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)
default:
return nil
}
}
)
)
@ -199,6 +222,8 @@ extension ChatListFilter {
flags |= self.data.categories.apiFlags
return .dialogFilter(flags: flags, id: self.id, title: self.title, includePeers: self.data.includePeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, excludePeers: self.data.excludePeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
})
}
}
@ -259,7 +284,7 @@ private func requestChatListFilters(postbox: Postbox, network: Network) -> Signa
let filter = ChatListFilter(apiFilter: apiFilter)
filters.append(filter)
switch apiFilter {
case let .dialogFilter(_, _, _, includePeers):
case let .dialogFilter(_, _, _, includePeers, excludePeers):
for peer in includePeers {
var peerId: PeerId?
switch peer {
@ -279,6 +304,25 @@ private func requestChatListFilters(postbox: Postbox, network: Network) -> Signa
}
}
}
for peer in excludePeers {
var peerId: PeerId?
switch peer {
case let .inputPeerUser(userId, _):
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
case let .inputPeerChat(chatId):
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)
case let .inputPeerChannel(channelId, _):
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)
default:
break
}
if let peerId = peerId {
if transaction.getPeer(peerId) == nil && !missingPeerIds.contains(peerId) {
missingPeerIds.insert(peerId)
missingPeers.append(peer)
}
}
}
}
}
return (filters, missingPeers)
@ -292,6 +336,8 @@ private func requestChatListFilters(postbox: Postbox, network: Network) -> Signa
}
func managedChatListFilters(postbox: Postbox, network: Network) -> Signal<Never, NoError> {
return .complete()
return requestChatListFilters(postbox: postbox, network: network)
|> `catch` { _ -> Signal<[ChatListFilter], NoError> in
return .complete()
@ -309,6 +355,8 @@ func managedChatListFilters(postbox: Postbox, network: Network) -> Signal<Never,
}
public func replaceRemoteChatListFilters(account: Account) -> Signal<Never, NoError> {
return .complete()
return account.postbox.transaction { transaction -> [ChatListFilter] in
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default
return settings.filters
@ -414,7 +462,7 @@ public func updateChatListFilterSettingsInteractively(postbox: Postbox, _ f: @es
return postbox.transaction { transaction -> ChatListFiltersState in
var result: ChatListFiltersState?
transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in
var settings = entry as? ChatListFiltersState ?? ChatListFiltersState.default
let settings = entry as? ChatListFiltersState ?? ChatListFiltersState.default
let updated = f(settings)
result = updated
return updated

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

@ -8379,12 +8379,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
var canEdit = false
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
if state.interfaceState.effectiveInputState.inputText.length == 0 && state.interfaceState.editMessage == nil {
canEdit = true
}
return state
})
if self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.length == 0 && self.presentationInterfaceState.interfaceState.editMessage == nil {
canEdit = true
}
if canEdit, let message = self.chatDisplayNode.historyNode.firstMessageForEditInCurrentHistoryView() {
inputShortcuts.append(KeyShortcut(input: UIKeyCommand.inputUpArrow, action: { [weak self] in

View file

@ -170,6 +170,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
}, peerSelected: { _ in
}, disabledPeerSelected: { _ in
}, togglePeerSelected: { _ in
}, additionalCategorySelected: { _ in
}, messageSelected: { [weak self] peer, message, _ in
if let strongSelf = self {
if let index = strongSelf.searchResult.messages.firstIndex(where: { $0.index == message.index }) {
@ -178,7 +179,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
strongSelf.listNode.clearHighlightAnimated(true)
}
}, groupSelected: { _ in
}, addContact: { [weak self] phoneNumber in
}, addContact: { _ in
}, setPeerIdWithRevealedOptions: { _, _ in
}, setItemPinned: { _, _ in
}, setPeerMuted: { _, _ in

View file

@ -131,7 +131,12 @@ public class ComposeController: ViewController {
}
})
strongSelf.createActionDisposable.set((controller.result
|> deliverOnMainQueue).start(next: { [weak controller] peerIds in
|> deliverOnMainQueue).start(next: { [weak controller] result in
var peerIds: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
peerIds = peerIdsValue
}
if let strongSelf = self, let controller = controller {
let createGroup = strongSelf.context.sharedContext.makeCreateGroupController(context: strongSelf.context, peerIds: peerIds.compactMap({ peerId in
if case let .peer(peerId) = peerId {

View file

@ -15,6 +15,7 @@ import ContactListUI
import CounterContollerTitleView
class ContactMultiselectionControllerImpl: ViewController, ContactMultiselectionController {
private let params: ContactMultiselectionControllerParams
private let context: AccountContext
private let mode: ContactMultiselectionControllerMode
@ -30,13 +31,14 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
private var _ready = Promise<Bool>()
private var _limitsReady = Promise<Bool>()
private var _peersReady = Promise<Bool>()
private var _listReady = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
private let _result = Promise<[ContactListPeerId]>()
var result: Signal<[ContactListPeerId], NoError> {
private let _result = Promise<ContactMultiselectionResult>()
var result: Signal<ContactMultiselectionResult, NoError> {
return self._result.get()
}
@ -62,10 +64,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
private var limitsConfiguration: LimitsConfiguration?
private var limitsConfigurationDisposable: Disposable?
private var initialPeersDisposable: Disposable?
private let options: [ContactListAdditionalOption]
private let filters: [ContactListFilter]
init(_ params: ContactMultiselectionControllerParams) {
self.params = params
self.context = params.context
self.mode = params.mode
self.options = params.options
@ -111,7 +115,28 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}
})
self._ready.set(combineLatest(self._listReady.get(), self._limitsReady.get()) |> map { $0 && $1 })
switch self.mode {
case let .chatSelection(selectedChats, _):
let _ = (self.context.account.postbox.transaction { transaction -> [Peer] in
return selectedChats.compactMap(transaction.getPeer)
}
|> deliverOnMainQueue).start(next: { [weak self] peers in
guard let strongSelf = self else {
return
}
strongSelf.contactsNode.editableTokens = peers.map { peer -> EditableTokenListToken in
return EditableTokenListToken(id: peer.id, title: peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder))
}
strongSelf._peersReady.set(.single(true))
if strongSelf.isNodeLoaded {
strongSelf.requestLayout(transition: .immediate)
}
})
default:
self._peersReady.set(.single(true))
}
self._ready.set(combineLatest(self._listReady.get(), self._limitsReady.get(), self._peersReady.get()) |> map { $0 && $1 && $2 })
}
required init(coder aDecoder: NSCoder) {
@ -121,6 +146,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
deinit {
self.presentationDataDisposable?.dispose()
self.limitsConfigurationDisposable?.dispose()
self.initialPeersDisposable?.dispose()
}
private func updateThemeAndStrings() {
@ -145,7 +171,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = count != 0
rightNavigationButton.isEnabled = count != 0 || self.params.alwaysEnabled
case .channelCreation:
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
@ -165,7 +191,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
self.rightNavigationButton = rightNavigationButton
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed))
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = false
rightNavigationButton.isEnabled = self.params.alwaysEnabled
}
}
@ -243,7 +269,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
if let updatedCount = updatedCount {
switch strongSelf.mode {
case .groupCreation, .peerSelection, .chatSelection:
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled
case .channelCreation:
break
}
@ -301,8 +327,8 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
if case let .peer(peerIdValue) = peerId {
if state.selectedPeerIds.contains(peerIdValue) {
state.selectedPeerIds.remove(peerIdValue)
removedTokenId = peerId
}
removedTokenId = peerIdValue
}
updatedCount = state.selectedPeerIds.count
var updatedState = ContactListNodeGroupSelectionState()
@ -322,7 +348,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
if let updatedCount = updatedCount {
switch strongSelf.mode {
case .groupCreation, .peerSelection, .chatSelection:
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled
case .channelCreation:
break
}
@ -344,6 +370,26 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}
}
self.contactsNode.additionalCategorySelected = { [weak self] id in
guard let strongSelf = self else {
return
}
switch strongSelf.contactsNode.contentNode {
case .contacts:
break
case let .chats(chatsNode):
chatsNode.updateState { state in
var state = state
if state.selectedAdditionalCategoryIds.contains(id) {
state.selectedAdditionalCategoryIds.remove(id)
} else {
state.selectedAdditionalCategoryIds.insert(id)
}
return state
}
}
}
self.displayNodeDidLoad()
}
@ -387,12 +433,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}
@objc func cancelPressed() {
self._result.set(.single([]))
self._result.set(.single(.none))
self.dismiss()
}
@objc func rightNavigationButtonPressed() {
var peerIds: [ContactListPeerId] = []
var additionalOptionIds: [Int] = []
switch self.contactsNode.contentNode {
case let .contacts(contactsNode):
contactsNode.updateSelectionState { state in
@ -405,7 +452,11 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
for peerId in chatsNode.currentState.selectedPeerIds {
peerIds.append(.peer(peerId))
}
for optionId in chatsNode.currentState.selectedAdditionalCategoryIds {
additionalOptionIds.append(optionId)
}
additionalOptionIds.sort()
}
self._result.set(.single(peerIds))
self._result.set(.single(.result(peerIds: peerIds, additionalOptionIds: additionalOptionIds)))
}
}

View file

@ -55,6 +55,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
var requestOpenPeerFromSearch: ((ContactListPeerId) -> Void)?
var openPeer: ((ContactListPeer) -> Void)?
var removeSelectedPeer: ((ContactListPeerId) -> Void)?
var additionalCategorySelected: ((Int) -> Void)?
var editableTokens: [EditableTokenListToken] = []
@ -82,9 +83,22 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
}
if case .chatSelection = mode {
if case let .chatSelection(selectedChats, additionalCategories) = mode {
placeholder = self.presentationData.strings.Common_Search
self.contentNode = .chats(ChatListNode(context: context, groupId: .root, previewing: false, controlsHistoryPreload: false, mode: .peers(filter: [.excludeSavedMessages], isSelecting: true), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true))
let chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, controlsHistoryPreload: false, mode: .peers(filter: [.excludeSavedMessages], isSelecting: true, additionalCategories: additionalCategories?.categories ?? []), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
chatListNode.updateState { state in
var state = state
for peerId in selectedChats {
state.selectedPeerIds.insert(peerId)
}
if let additionalCategories = additionalCategories {
for id in additionalCategories.selectedCategories {
state.selectedAdditionalCategoryIds.insert(id)
}
}
return state
}
self.contentNode = .chats(chatListNode)
} else {
self.contentNode = .contacts(ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList)), filters: filters, selectionState: ContactListNodeGroupSelectionState()))
}
@ -111,6 +125,12 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
chatsNode.peerSelected = { [weak self] peer, _, _ in
self?.openPeer?(.peer(peer: peer, isGlobal: false, participantCount: nil))
}
chatsNode.additionalCategorySelected = { [weak self] id in
guard let strongSelf = self else {
return
}
strongSelf.additionalCategorySelected?(id)
}
}
let searchText = ValuePromise<String>()

View file

@ -57,6 +57,7 @@ private final class TokenNode: ASDisplayNode {
didSet {
if self.isSelected != oldValue {
self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
self.titleNode.redrawIfPossible()
self.selectedBackgroundNode.isHidden = !self.isSelected
}
}
@ -67,6 +68,7 @@ private final class TokenNode: ASDisplayNode {
self.token = token
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.selectedBackgroundNode = ASImageNode()
self.selectedBackgroundNode.displaysAsynchronously = false

View file

@ -3449,7 +3449,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
if let contactsController = contactsController as? ContactMultiselectionController {
selectAddMemberDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak contactsController] peers in
|> deliverOnMainQueue).start(next: { [weak contactsController] result in
var peers: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
peers = peerIdsValue
}
contactsController?.displayProgress = true
addMemberDisposable.set((addMembers(peers)
|> deliverOnMainQueue).start(error: { error in

View file

@ -85,7 +85,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.segmentedControlNode = nil
}
self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, controlsHistoryPreload: false, mode: .peers(filter: filter, isSelecting: false), theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, controlsHistoryPreload: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: []), theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
super.init()

View file

@ -3,22 +3,17 @@ import Postbox
import SwiftSignalKit
public struct ChatListFilterSettings: Equatable, PreferencesEntry {
public var displayTabs: Bool
public static var `default`: ChatListFilterSettings {
return ChatListFilterSettings(displayTabs: true)
return ChatListFilterSettings()
}
public init(displayTabs: Bool) {
self.displayTabs = displayTabs
public init() {
}
public init(decoder: PostboxDecoder) {
self.displayTabs = decoder.decodeInt32ForKey("displayTabs", orElse: 1) != 0
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.displayTabs ? 1 : 0, forKey: "displayTabs")
}
public func isEqual(to: PreferencesEntry) -> Bool {

View file

@ -39,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: [.privateChat, .secretChat, .bot, .privateGroup], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true)
return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: .all, displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true)
}
public init(playSounds: Bool, vibrate: Bool, displayPreviews: Bool, totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: PeerSummaryCounterTags, displayNameOnLockscreen: Bool, displayNotificationsFromAllAccounts: Bool) {
@ -65,19 +65,21 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable {
var resultTags: PeerSummaryCounterTags = []
for legacyTag in LegacyPeerSummaryCounterTags(rawValue: value) {
if legacyTag == .regularChatsAndPrivateGroups {
resultTags.insert(.privateChat)
resultTags.insert(.secretChat)
resultTags.insert(.contact)
resultTags.insert(.nonContact)
resultTags.insert(.bot)
resultTags.insert(.privateGroup)
resultTags.insert(.smallGroup)
resultTags.insert(.largeGroup)
} else if legacyTag == .publicGroups {
resultTags.insert(.publicGroup)
resultTags.insert(.smallGroup)
resultTags.insert(.largeGroup)
} else if legacyTag == .channels {
resultTags.insert(.channel)
}
}
self.totalUnreadCountIncludeTags = resultTags
} else {
self.totalUnreadCountIncludeTags = [.privateChat, .secretChat, .bot, .privateGroup]
self.totalUnreadCountIncludeTags = .all
}
self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0
self.displayNotificationsFromAllAccounts = decoder.decodeInt32ForKey("displayNotificationsFromAllAccounts", orElse: 1) != 0

19
submodules/TooltipUI/BUCK Normal file
View file

@ -0,0 +1,19 @@
load("//Config:buck_rule_macros.bzl", "static_library")
static_library(
name = "TooltipUI",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
"//submodules/Display:Display#shared",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AppBundle:AppBundle",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
],
)

View file

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "TooltipUI",
module_name = "TooltipUI",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AppBundle:AppBundle",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,184 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AnimatedStickerNode
import AppBundle
private final class TooltipScreenNode: ViewControllerTracingNode {
private let location: CGPoint
private let requestDismiss: () -> Void
private let containerNode: ASDisplayNode
private let backgroundNode: ASImageNode
private let arrowNode: ASImageNode
private let animatedStickerNode: AnimatedStickerNode
private let textNode: ImmediateTextNode
init(text: String, location: CGPoint, requestDismiss: @escaping () -> Void) {
self.location = location
self.requestDismiss = requestDismiss
self.containerNode = ASDisplayNode()
let fillColor = UIColor(white: 0.0, alpha: 0.8)
self.backgroundNode = ASImageNode()
self.backgroundNode.image = generateAdjustedStretchableFilledCircleImage(diameter: 15.0, color: fillColor)
self.arrowNode = ASImageNode()
let arrowSize = CGSize(width: 29.0, height: 10.0)
self.arrowNode.image = generateImage(arrowSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(fillColor.cgColor)
context.scaleBy(x: 0.333, y: 0.333)
let _ = try? drawSvgPath(context, path: "M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ")
context.fillPath()
})
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
self.animatedStickerNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "ChatListFoldersTooltip", ofType: "json") {
self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct)
self.animatedStickerNode.automaticallyLoadFirstFrame = true
}
super.init()
self.backgroundNode.addSubnode(self.arrowNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.textNode)
self.containerNode.addSubnode(self.animatedStickerNode)
self.addSubnode(self.containerNode)
}
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
let sideInset: CGFloat = 13.0
let bottomInset: CGFloat = 10.0
let contentInset: CGFloat = 9.0
let contentVerticalInset: CGFloat = 9.0
let animationSize = CGSize(width: 32.0, height: 32.0)
let animationInset: CGFloat = (70 - animationSize.width) / 2.0
let animationSpacing: CGFloat = 8.0
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - contentInset * 2.0 - sideInset * 2.0 - animationSize.width - animationSpacing, height: .greatestFiniteMagnitude))
let backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0
let backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: self.location.y - bottomInset - backgroundHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: backgroundHeight))
transition.updateFrame(node: self.containerNode, frame: backgroundFrame)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
if let image = self.arrowNode.image {
let arrowSize = image.size
let arrowCenterX = self.location.x
transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: backgroundFrame.height), size: arrowSize))
}
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: contentVerticalInset), size: textSize))
transition.updateFrame(node: self.animatedStickerNode, frame: CGRect(origin: CGPoint(x: contentInset - animationInset, y: floor((backgroundHeight - animationSize.height) / 2.0) - animationInset), size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0)))
self.animatedStickerNode.updateLayout(size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0))
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let event = event {
var eventIsPresses = false
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
eventIsPresses = event.type == .presses
}
if event.type == .touches || eventIsPresses {
self.requestDismiss()
return nil
}
}
return super.hitTest(point, with: event)
}
func animateIn() {
self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.01)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: self.arrowNode.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowNode.frame.maxY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.6, additive: true)
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in
self?.animatedStickerNode.visibility = true
})
}
func animateOut(completion: @escaping () -> Void) {
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
self.containerNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.arrowNode.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowNode.frame.maxY - self.containerNode.bounds.height / 2.0), duration: 0.2, removeOnCompletion: false, additive: true)
}
}
public final class TooltipScreen: ViewController {
private let text: String
private let location: CGPoint
private var controllerNode: TooltipScreenNode {
return self.displayNode as! TooltipScreenNode
}
private var validLayout: ContainerViewLayout?
private var isDismissed: Bool = false
public init(text: String, location: CGPoint) {
self.text = text
self.location = location
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.controllerNode.animateIn()
}
override public func loadDisplayNode() {
self.displayNode = TooltipScreenNode(text: self.text, location: self.location, requestDismiss: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.dismiss()
})
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if let validLayout = self.validLayout {
if validLayout.size.width != layout.size.width {
self.dismiss()
}
}
self.validLayout = layout
self.controllerNode.updateLayout(layout: layout, transition: transition)
}
override public func dismiss(completion: (() -> Void)? = nil) {
if self.isDismissed {
return
}
self.isDismissed = true
self.controllerNode.animateOut(completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.presentingViewController?.dismiss(animated: false, completion: nil)
})
}
}

View file

@ -319,7 +319,7 @@ func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: P
}
func makeBridgeChat(_ entry: ChatListEntry, strings: PresentationStrings) -> (TGBridgeChat, [Int64 : TGBridgeUser])? {
if case let .MessageEntry(index, message, readState, _, _, renderedPeer, _, _, hasFailed) = entry {
if case let .MessageEntry(index, message, readState, _, _, renderedPeer, _, _, hasFailed, _) = entry {
guard index.messageIndex.id.peerId.namespace != Namespaces.Peer.SecretChat else {
return nil
}

View file

@ -78,7 +78,7 @@ genrule(
cp $(location :build-ton-bazel.sh) "$$BUILD_DIR/"
cp $(location :iOS-bazel.cmake) "$$BUILD_DIR/"
SOURCE_PATH="submodules/tonlib/tonlib-src"
SOURCE_PATH="submodules/ton/tonlib-src"
cp -R "$$SOURCE_PATH" "$$BUILD_DIR/"

View file

@ -1,6 +1,6 @@
#!/bin/sh
swift -swift-version 4 tools/GenerateLocalization.swift Telegram/Telegram-iOS/en.lproj/Localizable.strings submodules/TelegramPresentationData/Sources/PresentationStrings.swift submodules/TelegramUI/TelegramUI/Resources/PresentationStrings.mapping
swift -swift-version 4 tools/GenerateLocalization.swift Telegram/Telegram-iOS/en.lproj/Localizable.strings submodules/TelegramPresentationData/Sources/PresentationStrings.swift submodules/TelegramUI/Resources/PresentationStrings.mapping
mkdir -p submodules/WalletUI/Resources
swift -swift-version 4 tools/GenerateLocalization.swift Telegram/Telegram-iOS/en.lproj/Localizable.strings submodules/WalletUI/Sources/WalletStrings.swift submodules/WalletUI/Resources/WalletStrings.mapping "Wallet."