mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Update filters
This commit is contained in:
parent
d14e0772e7
commit
4397baa226
87 changed files with 3840 additions and 1931 deletions
2
.bazelrc
2
.bazelrc
|
|
@ -1,3 +1,5 @@
|
|||
build --experimental_guard_against_concurrent_changes
|
||||
|
||||
build --strategy=Genrule=local
|
||||
build --apple_platform_type=ios
|
||||
build --cxxopt='-std=c++14'
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ swift_library(
|
|||
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
|
||||
"//submodules/TelegramIntents:TelegramIntents",
|
||||
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/LargeGroup.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/LargeGroup.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_largegroup.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/LargeGroup.imageset/ic_largegroup.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/LargeGroup.imageset/ic_largegroup.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UnknownUser.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UnknownUser.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_noncontact.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UnknownUser.imageset/ic_noncontact.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UnknownUser.imageset/ic_noncontact.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/ChatListFilters.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/ChatListFilters.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_filters.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/ChatListFilters.imageset/ic_filters.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/ChatListFilters.imageset/ic_filters.pdf
vendored
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/ChatListFolders.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/ChatListFolders.tgs
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
19
submodules/TooltipUI/BUCK
Normal 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",
|
||||
],
|
||||
)
|
||||
19
submodules/TooltipUI/BUILD
Normal file
19
submodules/TooltipUI/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
184
submodules/TooltipUI/Sources/TooltipScreen.swift
Normal file
184
submodules/TooltipUI/Sources/TooltipScreen.swift
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/"
|
||||
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue