Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatListUI",
module_name = "ChatListUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramBaseController:TelegramBaseController",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/UndoUI:UndoUI",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/SearchUI:SearchUI",
"//submodules/MergeLists:MergeLists",
"//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/SearchBarNode:SearchBarNode",
"//submodules/ChatListSearchRecentPeersNode:ChatListSearchRecentPeersNode",
"//submodules/ChatListSearchItemNode:ChatListSearchItemNode",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager",
"//submodules/PeerPresenceStatusManager:PeerPresenceStatusManager",
"//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode",
"//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/ChatTitleActivityNode:ChatTitleActivityNode",
"//submodules/DeleteChatPeerActionSheetItem:DeleteChatPeerActionSheetItem",
"//submodules/LanguageSuggestionUI:LanguageSuggestionUI",
"//submodules/ContactsPeerItem:ContactsPeerItem",
"//submodules/ContactListUI:ContactListUI",
"//submodules/PhotoResources:PhotoResources",
"//submodules/AppBundle:AppBundle",
"//submodules/ContextUI:ContextUI",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/TelegramIntents:TelegramIntents",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TooltipUI:TooltipUI",
"//submodules/ListMessageItem:ListMessageItem",
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/GalleryData:GalleryData",
"//submodules/GalleryUI:GalleryUI",
"//submodules/InstantPageUI:InstantPageUI",
"//submodules/ListSectionHeaderNode:ListSectionHeaderNode",
"//submodules/ChatInterfaceState:ChatInterfaceState",
"//submodules/ShareController:ShareController",
"//submodules/GridMessageSelectionNode:GridMessageSelectionNode",
"//submodules/ChatListFilterSettingsHeaderItem:ChatListFilterSettingsHeaderItem",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/StickerResources:StickerResources",
"//submodules/TextFormat:TextFormat",
"//submodules/FetchManagerImpl:FetchManagerImpl",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/Components/ProgressIndicatorComponent:ProgressIndicatorComponent",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/PremiumUI:PremiumUI",
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/ForumCreateTopicScreen:ForumCreateTopicScreen",
"//submodules/TelegramUI/Components/ChatTitleView",
"//submodules/TelegramUI/Components/ChatTimerScreen",
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
"//submodules/AnimationUI:AnimationUI",
"//submodules/PeerInfoUI",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/TelegramUI/Components/ChatListTitleView",
"//submodules/AvatarNode:AvatarNode",
"//submodules/AvatarVideoNode:AvatarVideoNode",
"//submodules/InviteLinksUI",
"//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen",
"//submodules/ItemListUI",
"//submodules/QrCodeUI",
"//submodules/TelegramUI/Components/ActionPanelComponent",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
"//submodules/TelegramUI/Components/FullScreenEffectView",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
"//submodules/TelegramUI/Components/Settings/ArchiveInfoScreen",
"//submodules/TelegramUI/Components/Settings/NewSessionInfoScreen",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/AvatarUploadToastScreen",
"//submodules/TelegramUI/Components/Ads/AdsInfoScreen",
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
],
visibility = [
"//visibility:public",
],
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,379 @@
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 appearance: ChatListNodeAdditionalCategory.Appearance
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?,
appearance: ChatListNodeAdditionalCategory.Appearance,
isSelected: Bool,
header: ListViewItemHeader?,
action: @escaping () -> Void
) {
self.presentationData = presentationData
self.sectionId = sectionId
self.context = context
self.title = title
self.image = image
self.appearance = appearance
self.isSelected = isSelected
self.action = action
switch appearance {
case let .option(sectionTitle):
if let sectionTitle {
self.header = ChatListSearchItemHeader(type: .text(sectionTitle, AnyHashable(0)), theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
} else {
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
case .action:
self.header = header
}
}
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) {
if case .action = self.appearance {
listView.clearHighlightAnimated(true)
}
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 if let _ = nextItem as? ChatListAdditionalCategoryItem {
} else {
if let nextItem = nextItem as? ListViewItemWithHeader, nextItem.header != nil {
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) {
if let item = self.item, case .action = item.appearance {
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) {
let reallyHighlighted = self.isHighlighted
let highlightProgress: CGFloat = 1.0
if reallyHighlighted {
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
self.highlightedBackgroundNode.alpha = 0.0
}
self.highlightedBackgroundNode.layer.removeAllAnimations()
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: highlightProgress)
} else {
if self.highlightedBackgroundNode.supernode != nil {
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: 1.0 - highlightProgress, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
}
}
}
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
if case .option = item.appearance {
rightInset += 28.0
let selectionNode: CheckNode
if let current = currentSelectionNode {
selectionNode = current
updatedSelectionNode = selectionNode
} else {
selectionNode = CheckNode(theme: CheckNodeTheme(theme: item.presentationData.theme, style: .plain))
selectionNode.isUserInteractionEnabled = false
updatedSelectionNode = selectionNode
}
} else {
updatedSelectionNode = nil
}
var titleAttributedString: NSAttributedString?
let textColor: UIColor
if case .action = item.appearance {
textColor = item.presentationData.theme.list.itemAccentColor
} else {
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.topSeparatorNode.isHidden = true
if let image = item.image {
strongSelf.avatarNode.image = item.image
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0 + floor((avatarDiameter - image.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - image.size.width) / 2.0)), size: image.size))
}
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.setSelected(isSelected, animated: animated)
updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 22.0 - 17.0, y: floor((nodeLayout.contentSize.height - 22.0) / 2.0)), size: CGSize(width: 22.0, height: 22.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, options: ListViewItemAnimationOptions) {
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 headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
}
@@ -0,0 +1,467 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import AccountContext
import TelegramPresentationData
import SwiftSignalKit
import AnimationCache
import MultiAnimationRenderer
import TelegramCore
import Postbox
import ChatListHeaderComponent
import ActionPanelComponent
import ChatFolderLinkPreviewScreen
import EdgeEffect
import ComponentDisplayAdapters
final class ChatListContainerItemNode: ASDisplayNode {
private final class TopPanelItem {
let view = ComponentView<Empty>()
var size: CGSize?
init() {
}
}
private let context: AccountContext
private weak var controller: ChatListControllerImpl?
private let location: ChatListControllerLocation
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private var presentationData: PresentationData
private let becameEmpty: (ChatListFilter?) -> Void
private let emptyAction: (ChatListFilter?) -> Void
private let secondaryEmptyAction: () -> Void
private let openArchiveSettings: () -> Void
private let isInlineMode: Bool
private var floatingHeaderOffset: CGFloat?
private let edgeEffectView: EdgeEffectView
private(set) var emptyNode: ChatListEmptyNode?
var emptyShimmerEffectNode: ChatListShimmerNode?
private var shimmerNodeOffset: CGFloat = 0.0
let listNode: ChatListNode
private var topPanel: TopPanelItem?
private var pollFilterUpdatesDisposable: Disposable?
private var chatFilterUpdatesDisposable: Disposable?
private var peerDataDisposable: Disposable?
private var chatFolderUpdates: ChatFolderUpdates?
private var canReportPeer: Bool = false
private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)?
private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)?
init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool, isMainTab: Bool?) {
self.context = context
self.controller = controller
self.location = location
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.presentationData = presentationData
self.becameEmpty = becameEmpty
self.emptyAction = emptyAction
self.secondaryEmptyAction = secondaryEmptyAction
self.openArchiveSettings = openArchiveSettings
self.isInlineMode = isInlineMode
self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady, isMainTab: isMainTab)
if let controller, case .chatList(groupId: .root) = controller.location {
self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight
}
self.edgeEffectView = EdgeEffectView()
super.init()
self.addSubnode(self.listNode)
self.view.addSubview(self.edgeEffectView)
self.listNode.isEmptyUpdated = { [weak self] isEmptyState, _, transition in
guard let strongSelf = self else {
return
}
var needsShimmerNode = false
var shimmerNodeOffset: CGFloat = 0.0
var needsEmptyNode = false
var hasOnlyArchive = false
var hasOnlyGeneralThread = false
var isLoading = false
switch isEmptyState {
case let .empty(isLoadingValue, hasArchiveInfo):
if hasArchiveInfo {
shimmerNodeOffset = 253.0
}
if isLoadingValue {
needsShimmerNode = true
needsEmptyNode = false
isLoading = isLoadingValue
} else {
needsEmptyNode = true
}
if !isLoadingValue {
strongSelf.becameEmpty(filter)
}
case let .notEmpty(_, onlyHasArchiveValue, onlyGeneralThreadValue):
needsEmptyNode = onlyHasArchiveValue || onlyGeneralThreadValue
hasOnlyArchive = onlyHasArchiveValue
hasOnlyGeneralThread = onlyGeneralThreadValue
}
if needsEmptyNode {
if let currentNode = strongSelf.emptyNode {
currentNode.updateIsLoading(isLoading)
} else {
let subject: ChatListEmptyNode.Subject
if let filter = filter {
var showEdit = true
if case let .filter(_, _, _, data) = filter {
if data.excludeRead && data.includePeers.peers.isEmpty && data.includePeers.pinnedPeers.isEmpty {
showEdit = false
}
}
subject = .filter(showEdit: showEdit)
} else {
if case .forum = location {
subject = .forum(hasGeneral: hasOnlyGeneralThread)
} else {
if case .chatList(groupId: .archive) = location {
subject = .archive
} else {
subject = .chats(hasArchive: hasOnlyArchive)
}
}
}
let emptyNode = ChatListEmptyNode(context: context, subject: subject, isLoading: isLoading, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, action: {
self?.emptyAction(filter)
}, secondaryAction: {
self?.secondaryEmptyAction()
}, openArchiveSettings: {
self?.openArchiveSettings()
})
strongSelf.emptyNode = emptyNode
strongSelf.listNode.addSubnode(emptyNode)
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
emptyNode.frame = emptyNodeFrame
emptyNode.updateLayout(size: size, insets: insets, transition: .immediate)
if let scrollingOffset = strongSelf.scrollingOffset {
emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
}
}
emptyNode.alpha = 0.0
transition.updateAlpha(node: emptyNode, alpha: 1.0)
}
} else if let emptyNode = strongSelf.emptyNode {
strongSelf.emptyNode = nil
transition.updateAlpha(node: emptyNode, alpha: 0.0, completion: { [weak emptyNode] _ in
emptyNode?.removeFromSupernode()
})
}
if needsShimmerNode {
strongSelf.shimmerNodeOffset = shimmerNodeOffset
if strongSelf.emptyShimmerEffectNode == nil {
let emptyShimmerEffectNode = ChatListShimmerNode()
strongSelf.emptyShimmerEffectNode = emptyShimmerEffectNode
strongSelf.insertSubnode(emptyShimmerEffectNode, belowSubnode: strongSelf.listNode)
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let offset = strongSelf.floatingHeaderOffset {
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: .immediate)
}
}
} else if let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
strongSelf.emptyShimmerEffectNode = nil
let emptyNodeTransition = transition.isAnimated ? transition : .animated(duration: 0.3, curve: .easeInOut)
emptyNodeTransition.updateAlpha(node: emptyShimmerEffectNode, alpha: 0.0, completion: { [weak emptyShimmerEffectNode] _ in
emptyShimmerEffectNode?.removeFromSupernode()
})
strongSelf.listNode.alpha = 0.0
emptyNodeTransition.updateAlpha(node: strongSelf.listNode, alpha: 1.0)
}
}
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
guard let strongSelf = self else {
return
}
strongSelf.floatingHeaderOffset = offset
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: transition)
}
strongSelf.layoutAdditionalPanels(transition: transition)
}
if let filter, case let .filter(id, _, _, data) = filter, data.isShared {
self.pollFilterUpdatesDisposable = self.context.engine.peers.pollChatFolderUpdates(folderId: id).startStrict()
self.chatFilterUpdatesDisposable = (self.context.engine.peers.subscribedChatFolderUpdates(folderId: id)
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let self else {
return
}
var update = false
if let result, result.availableChatsToJoin != 0 {
if self.chatFolderUpdates?.availableChatsToJoin != result.availableChatsToJoin {
update = true
}
self.chatFolderUpdates = result
} else {
if self.chatFolderUpdates != nil {
self.chatFolderUpdates = nil
update = true
}
}
if update {
if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
if case let .forum(peerId) = location {
self.peerDataDisposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.StatusSettings(id: peerId)
)
|> deliverOnMainQueue).startStrict(next: { [weak self] statusSettings in
guard let self else {
return
}
var canReportPeer = false
if let statusSettings, statusSettings.flags.contains(.canReport) {
canReportPeer = true
}
if self.canReportPeer != canReportPeer {
self.canReportPeer = canReportPeer
if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
}
deinit {
self.pollFilterUpdatesDisposable?.dispose()
self.chatFilterUpdatesDisposable?.dispose()
self.peerDataDisposable?.dispose()
}
private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) {
node.update(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, size: size, isInlineMode: self.isInlineMode, presentationData: self.presentationData, transition: .immediate)
transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size))
}
private func layoutAdditionalPanels(transition: ContainedViewLayoutTransition) {
guard let (size, insets, visualNavigationHeight, _, _, _, _) = self.validLayout, let offset = self.floatingHeaderOffset else {
return
}
let _ = size
let _ = insets
if let topPanel = self.topPanel, let topPanelSize = topPanel.size {
let minY: CGFloat = visualNavigationHeight - 44.0 + topPanelSize.height
if let topPanelView = topPanel.view.view {
var animateIn = false
var topPanelTransition = transition
if topPanelView.bounds.isEmpty {
topPanelTransition = .immediate
animateIn = true
}
topPanelTransition.updateFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(minY, offset - topPanelSize.height)), size: topPanelSize))
if animateIn {
transition.animatePositionAdditive(layer: topPanelView.layer, offset: CGPoint(x: 0.0, y: -topPanelView.bounds.height))
}
}
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.listNode.updateThemeAndStrings(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
self.emptyNode?.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset)
var listInsets = insets
var additionalTopInset: CGFloat = 0.0
if let chatFolderUpdates = self.chatFolderUpdates {
let topPanel: TopPanelItem
var topPanelTransition = ComponentTransition(transition)
if let current = self.topPanel {
topPanel = current
} else {
topPanelTransition = .immediate
topPanel = TopPanelItem()
self.topPanel = topPanel
}
let title: String = self.presentationData.strings.ChatList_PanelNewChatsAvailable(Int32(chatFolderUpdates.availableChatsToJoin))
let topPanelHeight: CGFloat = 44.0
let _ = topPanel.view.update(
transition: topPanelTransition,
component: AnyComponent(ActionPanelComponent(
theme: self.presentationData.theme,
title: title,
color: .accent,
action: { [weak self] in
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
return
}
self.listNode.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(chatFolderUpdates), contents: chatFolderUpdates.chatFolderLinkContents))
},
dismissAction: { [weak self] in
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
return
}
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: chatFolderUpdates.folderId).startStandalone()
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: topPanelHeight)
)
if let topPanelView = topPanel.view.view {
if topPanelView.superview == nil {
self.view.addSubview(topPanelView)
}
}
topPanel.size = CGSize(width: size.width, height: topPanelHeight)
listInsets.top += topPanelHeight
additionalTopInset += topPanelHeight
} else if self.canReportPeer {
let topPanel: TopPanelItem
var topPanelTransition = ComponentTransition(transition)
if let current = self.topPanel {
topPanel = current
} else {
topPanelTransition = .immediate
topPanel = TopPanelItem()
self.topPanel = topPanel
}
let title: String = self.presentationData.strings.Conversation_ReportSpamAndLeave
let topPanelHeight: CGFloat = 44.0
let _ = topPanel.view.update(
transition: topPanelTransition,
component: AnyComponent(ActionPanelComponent(
theme: self.presentationData.theme,
title: title,
color: .destructive,
action: { [weak self] in
guard let self, case let .forum(peerId) = self.location else {
return
}
let actionSheet = ActionSheetController(presentationData: self.presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: self.presentationData.strings.Conversation_ReportSpamGroupConfirmation),
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
if let self {
self.controller?.setInlineChatList(location: nil)
let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: true).startStandalone()
}
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.listNode.present?(actionSheet)
},
dismissAction: { [weak self] in
guard let self, case let .forum(peerId) = self.location else {
return
}
let _ = self.context.engine.peers.dismissPeerStatusOptions(peerId: peerId).startStandalone()
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: topPanelHeight)
)
if let topPanelView = topPanel.view.view {
if topPanelView.superview == nil {
self.view.addSubview(topPanelView)
}
}
topPanel.size = CGSize(width: size.width, height: topPanelHeight)
listInsets.top += topPanelHeight
additionalTopInset += topPanelHeight
} else {
if let topPanel = self.topPanel {
self.topPanel = nil
if let topPanelView = topPanel.view.view {
transition.updatePosition(layer: topPanelView.layer, position: CGPoint(x: topPanelView.layer.position.x, y: topPanelView.layer.position.y - topPanelView.layer.bounds.height), completion: { [weak topPanelView] _ in
topPanelView?.removeFromSuperview()
})
}
}
}
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: listInsets, duration: duration, curve: curve)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight + additionalTopInset, originalTopInset: originalNavigationHeight + additionalTopInset, storiesInset: storiesInset, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction)
if let emptyNode = self.emptyNode {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
transition.updateFrame(node: emptyNode, frame: emptyNodeFrame)
emptyNode.updateLayout(size: emptyNodeFrame.size, insets: listInsets, transition: transition)
if let scrollingOffset = self.scrollingOffset {
emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: transition)
}
}
self.layoutAdditionalPanels(transition: transition)
let edgeEffectHeight: CGFloat = insets.bottom
let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - edgeEffectHeight), size: CGSize(width: size.width, height: edgeEffectHeight))
transition.updateFrame(view: self.edgeEffectView, frame: edgeEffectFrame)
self.edgeEffectView.update(content: self.presentationData.theme.list.plainBackgroundColor, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: ComponentTransition(transition))
transition.updateAlpha(layer: self.edgeEffectView.layer, alpha: edgeEffectHeight > 21.0 ? 1.0 : 0.0)
}
func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.scrollingOffset = (navigationHeight, offset)
if let emptyNode = self.emptyNode {
emptyNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: offset, transition: transition)
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,162 @@
import Foundation
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import StoryContainerScreen
import StoryStealthModeSheetScreen
import UndoUI
extension ChatListControllerImpl {
func requestStealthMode(openStory: @escaping (@escaping (StoryContainerScreen) -> Void) -> Void) {
let context = self.context
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(),
TelegramEngine.EngineData.Item.Configuration.App()
)
|> deliverOnMainQueue).start(next: { [weak self] config, appConfig in
guard let self else {
return
}
let timestamp = Int32(Date().timeIntervalSince1970)
if let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp {
let remainingActiveSeconds = activeUntilTimestamp - timestamp
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
let text = presentationData.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string
let tooltipScreen = UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(title: presentationData.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in
return false
}
)
tooltipScreen.tag = "no_auto_dismiss"
let tooltipScreenValue: UndoOverlayController? = tooltipScreen
self.currentTooltipUpdateTimer?.invalidate()
self.currentTooltipUpdateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self, weak tooltipScreenValue] _ in
guard let self else {
return
}
guard let tooltipScreenValue else {
self.currentTooltipUpdateTimer?.invalidate()
self.currentTooltipUpdateTimer = nil
return
}
let timestamp = Int32(Date().timeIntervalSince1970)
let remainingActiveSeconds = max(1, activeUntilTimestamp - timestamp)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
let text = presentationData.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string
tooltipScreenValue.content = .actionSucceeded(title: presentationData.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false)
})
openStory({ storyController in
storyController.presentExternalTooltip(tooltipScreen)
})
return
}
let pastPeriod: Int32
let futurePeriod: Int32
if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double {
futurePeriod = Int32(futurePeriodF)
pastPeriod = Int32(pastPeriodF)
} else {
pastPeriod = 5 * 60
futurePeriod = 25 * 60
}
let sheet = StoryStealthModeSheetScreen(
context: context,
mode: .control(external: true, cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp),
forceDark: false,
backwardDuration: pastPeriod,
forwardDuration: futurePeriod,
buttonAction: {
let _ = (context.engine.messages.enableStoryStealthMode()
|> deliverOnMainQueue).start(completed: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
let text = presentationData.strings.Story_ToastStealthModeActivatedText(timeIntervalString(strings: presentationData.strings, value: pastPeriod), timeIntervalString(strings: presentationData.strings, value: futurePeriod)).string
let tooltipScreen = UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(title: presentationData.strings.Story_ToastStealthModeActivatedTitle, text: text, cancel: "", destructive: false),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in
return false
}
)
openStory({ storyController in
storyController.presentExternalTooltip(tooltipScreen)
})
HapticFeedback().success()
})
}
)
self.push(sheet)
})
}
func presentStealthModeUpgrade(action: @escaping () -> Void) {
let context = self.context
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(),
TelegramEngine.EngineData.Item.Configuration.App()
)
|> deliverOnMainQueue).start(next: { [weak self] config, appConfig in
guard let self else {
return
}
let pastPeriod: Int32
let futurePeriod: Int32
if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double {
futurePeriod = Int32(futurePeriodF)
pastPeriod = Int32(pastPeriodF)
} else {
pastPeriod = 5 * 60
futurePeriod = 25 * 60
}
let sheet = StoryStealthModeSheetScreen(
context: context,
mode: .upgrade,
forceDark: false,
backwardDuration: pastPeriod,
forwardDuration: futurePeriod,
buttonAction: {
action()
}
)
self.push(sheet)
})
}
func presentUpgradeStoriesScreen() {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .stories, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesStealthMode, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak self, weak controller] c in
controller?.dismiss(animated: true, completion: {
guard let self else {
return
}
self.push(c)
})
}
self.push(controller)
}
}
@@ -0,0 +1,374 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AppBundle
import SolidRoundedButtonNode
import ActivityIndicator
import AccountContext
import TelegramCore
import ComponentFlow
import ArchiveInfoScreen
import ComponentDisplayAdapters
import SwiftSignalKit
import ChatListHeaderComponent
final class ChatListEmptyNode: ASDisplayNode {
enum Subject {
case chats(hasArchive: Bool)
case archive
case filter(showEdit: Bool)
case forum(hasGeneral: Bool)
}
private let action: () -> Void
private let secondaryAction: () -> Void
private let openArchiveSettings: () -> Void
private let context: AccountContext
private var theme: PresentationTheme
private var strings: PresentationStrings
let subject: Subject
private(set) var isLoading: Bool
private let textNode: ImmediateTextNode
private let descriptionNode: ImmediateTextNode
private let animationNode: AnimatedStickerNode
private let buttonNode: SolidRoundedButtonNode
private let secondaryButtonNode: HighlightableButtonNode
private let activityIndicator: ActivityIndicator
private var emptyArchive: ComponentView<Empty>?
private var animationSize: CGSize = CGSize()
private var buttonIsHidden: Bool
private var validLayout: (size: CGSize, insets: UIEdgeInsets)?
private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)?
private var globalPrivacySettings: GlobalPrivacySettings = .default
private var archiveSettingsDisposable: Disposable?
init(context: AccountContext, subject: Subject, isLoading: Bool, theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void, secondaryAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void) {
self.context = context
self.theme = theme
self.strings = strings
self.action = action
self.secondaryAction = secondaryAction
self.openArchiveSettings = openArchiveSettings
self.subject = subject
self.isLoading = isLoading
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.textNode.isUserInteractionEnabled = false
self.textNode.textAlignment = .center
self.textNode.lineSpacing = 0.1
self.descriptionNode = ImmediateTextNode()
self.descriptionNode.displaysAsynchronously = false
self.descriptionNode.maximumNumberOfLines = 0
self.descriptionNode.isUserInteractionEnabled = false
self.descriptionNode.textAlignment = .center
self.descriptionNode.lineSpacing = 0.1
var gloss = true
if case .filter = subject {
gloss = false
} else if case .chats(true) = subject {
gloss = false
}
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), cornerRadius: 11.0, isShimmering: gloss)
self.secondaryButtonNode = HighlightableButtonNode()
self.activityIndicator = ActivityIndicator(type: .custom(theme.list.itemAccentColor, 22.0, 1.0, false))
var buttonIsHidden = false
let animationName: String
if case let .filter(showEdit) = subject {
animationName = "ChatListFilterEmpty"
buttonIsHidden = !showEdit
} else {
animationName = "ChatListEmpty"
}
self.buttonIsHidden = buttonIsHidden
super.init()
self.animationSize = CGSize(width: 124.0, height: 124.0)
if case .archive = subject {
} else {
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
self.addSubnode(self.descriptionNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.secondaryButtonNode)
self.addSubnode(self.activityIndicator)
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
}
self.animationNode.isHidden = self.isLoading
self.textNode.isHidden = self.isLoading
self.descriptionNode.isHidden = self.isLoading
self.buttonNode.isHidden = self.buttonIsHidden || self.isLoading
self.activityIndicator.isHidden = !self.isLoading
self.buttonNode.hitTestSlop = UIEdgeInsets(top: -10.0, left: -10.0, bottom: -10.0, right: -10.0)
self.buttonNode.pressed = { [weak self] in
self?.buttonPressed()
}
self.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside)
self.updateThemeAndStrings(theme: theme, strings: strings)
self.animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationTapGesture(_:))))
if case .archive = subject {
let _ = self.context.engine.privacy.updateGlobalPrivacySettings().startStandalone()
self.archiveSettingsDisposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Configuration.GlobalPrivacy()
)
|> deliverOnMainQueue).startStrict(next: { [weak self] settings in
guard let self else {
return
}
self.globalPrivacySettings = settings
if let (size, insets) = self.validLayout {
self.updateLayout(size: size, insets: insets, transition: .immediate)
}
})
}
}
deinit {
self.archiveSettingsDisposable?.dispose()
}
@objc private func buttonPressed() {
self.action()
}
@objc private func secondaryButtonPressed() {
self.secondaryAction()
}
@objc private func animationTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if !self.animationNode.isPlaying {
self.animationNode.play(firstFrame: false, fromIndex: nil)
}
}
}
func restartAnimation() {
self.animationNode.play(firstFrame: false, fromIndex: nil)
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
let text: String
var descriptionText = ""
let buttonText: String?
switch self.subject {
case let .chats(hasArchive):
text = hasArchive ? strings.ChatList_EmptyChatListWithArchive : strings.ChatList_EmptyChatList
buttonText = strings.ChatList_EmptyChatListNewMessage
case .archive:
text = strings.ChatList_EmptyChatList
buttonText = nil
case .filter:
text = strings.ChatList_EmptyChatListFilterTitle
descriptionText = strings.ChatList_EmptyChatListFilterText
buttonText = strings.ChatList_EmptyChatListEditFilter
case .forum:
text = strings.ChatList_EmptyTopicsTitle
buttonText = strings.ChatList_EmptyTopicsCreate
descriptionText = strings.ChatList_EmptyTopicsDescription
}
let string = NSMutableAttributedString(string: text, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor)
let descriptionString = NSAttributedString(string: descriptionText, font: Font.regular(14.0), textColor: theme.list.itemSecondaryTextColor)
self.textNode.attributedText = string
self.descriptionNode.attributedText = descriptionString
if let buttonText {
self.buttonNode.title = buttonText
self.buttonNode.isHidden = false
} else {
self.buttonNode.isHidden = true
}
self.activityIndicator.type = .custom(theme.list.itemAccentColor, 22.0, 1.0, false)
if let (size, insets) = self.validLayout {
self.updateLayout(size: size, insets: insets, transition: .immediate)
if let scrollingOffset = self.scrollingOffset {
self.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
}
}
}
func updateIsLoading(_ isLoading: Bool) {
if self.isLoading == isLoading {
return
}
self.isLoading = isLoading
self.animationNode.isHidden = self.isLoading
self.textNode.isHidden = self.isLoading
self.descriptionNode.isHidden = self.isLoading
self.buttonNode.isHidden = self.buttonIsHidden || self.isLoading
self.activityIndicator.isHidden = !self.isLoading
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets)
let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: insets.top + floor((size.height - insets.top - insets.bottom - indicatorSize.height - 50.0) / 2.0)), size: indicatorSize))
let animationSpacing: CGFloat = 24.0
let descriptionSpacing: CGFloat = 8.0
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 40.0, height: size.height - insets.top - insets.bottom))
let descriptionSize = self.descriptionNode.updateLayout(CGSize(width: size.width - 40.0, height: size.height - insets.top - insets.bottom))
let buttonSideInset: CGFloat = 32.0
let buttonWidth = min(270.0, size.width - buttonSideInset * 2.0)
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
let buttonSize = CGSize(width: buttonWidth, height: buttonHeight)
let secondaryButtonSize = self.secondaryButtonNode.measure(CGSize(width: buttonWidth, height: .greatestFiniteMagnitude))
var threshold: CGFloat = 0.0
if case .forum = self.subject {
threshold = 80.0
}
let contentHeight = self.animationSize.height + animationSpacing + textSize.height + buttonSize.height
var contentOffset: CGFloat = 0.0
if size.height - insets.top - insets.bottom < contentHeight + threshold {
contentOffset = -self.animationSize.height - animationSpacing + 44.0
transition.updateAlpha(node: self.animationNode, alpha: 0.0)
} else {
contentOffset = -40.0
transition.updateAlpha(node: self.animationNode, alpha: 1.0)
}
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - self.animationSize.width) / 2.0), y: insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0) + contentOffset), size: self.animationSize)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: animationFrame.maxY + animationSpacing), size: textSize)
let descriptionFrame = CGRect(origin: CGPoint(x: floor((size.width - descriptionSize.width) / 2.0), y: textFrame.maxY + descriptionSpacing), size: descriptionSize)
if !self.animationSize.width.isZero {
self.animationNode.updateLayout(size: self.animationSize)
transition.updateFrame(node: self.animationNode, frame: animationFrame)
}
transition.updateFrame(node: self.textNode, frame: textFrame)
transition.updateFrame(node: self.descriptionNode, frame: descriptionFrame)
var bottomInset: CGFloat = 16.0
let secondaryButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - secondaryButtonSize.width) / 2.0), y: size.height - insets.bottom - secondaryButtonSize.height - bottomInset), size: secondaryButtonSize)
transition.updateFrame(node: self.secondaryButtonNode, frame: secondaryButtonFrame)
if secondaryButtonSize.height > 0.0 {
bottomInset += secondaryButtonSize.height + 23.0
}
let buttonFrame: CGRect
if case .forum = self.subject {
buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: descriptionFrame.maxY + 20.0), size: buttonSize)
} else {
buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - insets.bottom - buttonHeight - bottomInset), size: buttonSize)
}
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
}
func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.scrollingOffset = (navigationHeight, offset)
guard let (size, _) = self.validLayout else {
return
}
if case .archive = self.subject {
let emptyArchive: ComponentView<Empty>
if let current = self.emptyArchive {
emptyArchive = current
} else {
emptyArchive = ComponentView()
self.emptyArchive = emptyArchive
}
let emptyArchiveSize = emptyArchive.update(
transition: ComponentTransition(transition),
component: AnyComponent(ArchiveInfoContentComponent(
theme: self.theme,
strings: self.strings,
settings: self.globalPrivacySettings,
openSettings: { [weak self] in
guard let self else {
return
}
self.openArchiveSettings()
}
)),
environment: {
},
containerSize: CGSize(width: size.width, height: 10000.0)
)
if let emptyArchiveView = emptyArchive.view {
if emptyArchiveView.superview == nil {
self.view.addSubview(emptyArchiveView)
}
let cancelledOutHeight: CGFloat = max(0.0, ChatListNavigationBar.searchScrollHeight - offset)
let visibleNavigationHeight: CGFloat = navigationHeight - ChatListNavigationBar.searchScrollHeight + cancelledOutHeight
let additionalOffset = min(0.0, -offset + ChatListNavigationBar.searchScrollHeight)
var archiveFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleNavigationHeight + floorToScreenPixels((size.height - visibleNavigationHeight - emptyArchiveSize.height - 50.0) * 0.5)), size: emptyArchiveSize)
archiveFrame.origin.y = max(archiveFrame.origin.y, visibleNavigationHeight + 20.0)
if size.height - visibleNavigationHeight - emptyArchiveSize.height - 20.0 < 0.0 {
archiveFrame.origin.y += additionalOffset
}
transition.updateFrame(view: emptyArchiveView, frame: archiveFrame)
}
} else if let emptyArchive = self.emptyArchive {
self.emptyArchive = nil
emptyArchive.view?.removeFromSuperview()
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.buttonNode.frame.contains(point) {
return self.buttonNode.view.hitTest(self.view.convert(point, to: self.buttonNode.view), with: event)
}
if self.secondaryButtonNode.frame.contains(point), !self.secondaryButtonNode.isHidden {
return self.secondaryButtonNode.view.hitTest(self.view.convert(point, to: self.secondaryButtonNode.view), with: event)
}
if let emptyArchiveView = self.emptyArchive?.view {
if let result = emptyArchiveView.hitTest(self.view.convert(point, to: emptyArchiveView), with: event) {
return result
}
}
return nil
}
}
@@ -0,0 +1,474 @@
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 groups
case channels
case bots
case muted
case read
case archived
}
final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
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,
systemStyle: ItemListSystemStyle,
title: String,
icon: ChatListFilterCategoryIcon,
isRevealed: Bool,
sectionId: ItemListSectionId,
updatedRevealedOptions: @escaping (Bool) -> Void,
remove: @escaping () -> Void
) {
self.presentationData = presentationData
self.systemStyle = systemStyle
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.maskNode.isUserInteractionEnabled = false
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 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
var verticalInset: CGFloat = 14.0
let verticalOffset: CGFloat
let avatarSize: CGFloat
if case .glass = item.systemStyle {
verticalInset += 4.0
}
verticalOffset = 0.0
avatarSize = 40.0
leftInset = 65.0 + params.leftInset
let editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? = nil
let editingOffset: CGFloat
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, params)
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 separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
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 List/Filters/Contact"
case .nonContacts:
color = .yellow
imageName = "Chat List/Filters/User"
case .groups:
color = .green
imageName = "Chat List/Filters/Group"
case .channels:
color = .red
imageName = "Chat List/Filters/Channel"
case .bots:
color = .violet
imageName = "Chat List/Filters/Bot"
case .muted:
color = .red
imageName = "Chat List/Filters/Muted"
case .read:
color = .blue
imageName = "Chat List/Filters/Read"
case .archived:
color = .yellow
imageName = "Chat List/Filters/Archive"
}
updatedAvatarImage = generateAvatarImage(size: CGSize(width: avatarSize, height: avatarSize), icon: generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white), cornerRadius: 12.0, 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.addSubnode(strongSelf.maskNode)
}
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
strongSelf.bottomStripeNode.isHidden = false
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, glass: item.systemStyle == .glass) : 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 - params.rightInset - separatorRightInset, 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, options: ListViewItemAnimationOptions) {
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 headers() -> [ListViewItemHeader]? {
return nil
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,824 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import AccountContext
import ItemListPeerActionItem
import ChatListFilterSettingsHeaderItem
import PremiumUI
import UndoUI
import ChatFolderLinkPreviewScreen
private final class ChatListFilterPresetListControllerArguments {
let context: AccountContext
let addSuggestedPressed: (ChatFolderTitle, ChatListFilterData) -> Void
let openPreset: (ChatListFilter) -> Void
let addNew: () -> Void
let setItemWithRevealedOptions: (Int32?, Int32?) -> Void
let removePreset: (Int32) -> Void
let updateDisplayTags: (Bool) -> Void
let updateDisplayTagsLocked: () -> Void
init(context: AccountContext, addSuggestedPressed: @escaping (ChatFolderTitle, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) {
self.context = context
self.addSuggestedPressed = addSuggestedPressed
self.openPreset = openPreset
self.addNew = addNew
self.setItemWithRevealedOptions = setItemWithRevealedOptions
self.removePreset = removePreset
self.updateDisplayTags = updateDisplayTags
self.updateDisplayTagsLocked = updateDisplayTagsLocked
}
}
private enum ChatListFilterPresetListSection: Int32 {
case screenHeader
case suggested
case list
case tags
}
public enum ChatListFilterPresetListEntryTag: ItemListItemTag {
case displayTags
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ChatListFilterPresetListEntryTag, self == other {
return true
} else {
return false
}
}
}
private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
if peers.isEmpty {
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
} else {
var result = 0
for (_, peer) in peers {
result += peer.userCount
}
return strings.UserCount(Int32(result))
}
}
private enum ChatListFilterPresetListEntryStableId: Hashable {
case screenHeader
case suggestedListHeader
case suggestedPreset(ChatListFilterData)
case suggestedAddCustom
case listHeader
case addItem
case preset(Int32)
case listFooter
case displayTags
case displayTagsFooter
}
private struct PresetIndex: Equatable {
let value: Int
static func ==(lhs: PresetIndex, rhs: PresetIndex) -> Bool {
return true
}
}
private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case screenHeader(String)
case suggestedListHeader(String)
case suggestedPreset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilterData)
case suggestedAddCustom(String)
case listHeader(String)
case preset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool)
case addItem(text: String, isEditing: Bool)
case listFooter(String)
case displayTags(Bool?)
case displayTagsFooter
var section: ItemListSectionId {
switch self {
case .screenHeader:
return ChatListFilterPresetListSection.screenHeader.rawValue
case .suggestedListHeader, .suggestedPreset, .suggestedAddCustom:
return ChatListFilterPresetListSection.suggested.rawValue
case .listHeader, .preset, .addItem, .listFooter:
return ChatListFilterPresetListSection.list.rawValue
case .displayTags, .displayTagsFooter:
return ChatListFilterPresetListSection.tags.rawValue
}
}
var sortId: Int {
switch self {
case .screenHeader:
return 0
case .listHeader:
return 100
case .addItem:
return 101
case let .preset(index, _, _, _, _, _, _, _, _, _):
return 102 + index.value
case .listFooter:
return 1001
case .suggestedListHeader:
return 1002
case let .suggestedPreset(index, _, _, _):
return 1003 + index.value
case .suggestedAddCustom:
return 2000
case .displayTags:
return 3000
case .displayTagsFooter:
return 3001
}
}
var stableId: ChatListFilterPresetListEntryStableId {
switch self {
case .screenHeader:
return .screenHeader
case .suggestedListHeader:
return .suggestedListHeader
case let .suggestedPreset(_, _, _, preset):
return .suggestedPreset(preset)
case .suggestedAddCustom:
return .suggestedAddCustom
case .listHeader:
return .listHeader
case let .preset(_, _, _, preset, _, _, _, _, _, _):
return .preset(preset.id)
case .addItem:
return .addItem
case .listFooter:
return .listFooter
case .displayTags:
return .displayTags
case .displayTagsFooter:
return .displayTagsFooter
}
}
static func <(lhs: ChatListFilterPresetListEntry, rhs: ChatListFilterPresetListEntry) -> Bool {
return lhs.sortId < rhs.sortId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChatListFilterPresetListControllerArguments
switch self {
case let .screenHeader(text):
return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animation: .folders, 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, systemStyle: .glass, title: title.text, label: label, sectionId: self.section, style: .blocks, installAction: {
arguments.addSuggestedPressed(title, preset)
}, tag: nil)
case let .suggestedAddCustom(text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: text, sectionId: self.section, height: .generic, editing: false, action: {
arguments.addNew()
})
case let .listHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats, isDisabled, displayTags):
var resolvedColor: UIColor?
if displayTags, case let .filter(_, _, _, data) = preset {
let tagColor = data.color
if let tagColor {
resolvedColor = arguments.context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance).main
}
}
return ChatListFilterPresetListItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: {
if isDisabled {
arguments.addNew()
} else {
arguments.openPreset(preset)
}
}, setItemWithRevealedOptions: { lhs, rhs in
arguments.setItemWithRevealedOptions(lhs, rhs)
}, remove: {
arguments.removePreset(preset.id)
})
case let .addItem(text, isEditing):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: text, sectionId: self.section, height: .generic, editing: isEditing, action: {
arguments.addNew()
})
case let .listFooter(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .displayTags(value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.ChatListFilterList_ShowTags, value: value == true, enableInteractiveChanges: value != nil, enabled: true, displayLocked: value == nil, sectionId: self.section, style: .blocks, updated: { updatedValue in
if value != nil {
arguments.updateDisplayTags(updatedValue)
} else {
arguments.updateDisplayTagsLocked()
}
}, activatedWhileDisabled: {
arguments.updateDisplayTagsLocked()
}, tag: ChatListFilterPresetListEntryTag.displayTags)
case .displayTagsFooter:
return ItemListTextItem(presentationData: presentationData, text: .plain(presentationData.strings.ChatListFilterList_ShowTagsFooter), sectionId: self.section)
}
}
}
private struct ChatListFilterPresetListControllerState: Equatable {
var isEditing: Bool = false
var revealedPreset: Int32? = nil
}
private func filtersWithAppliedOrder(filters: [(ChatListFilter, Int)], order: [Int32]?) -> [(ChatListFilter, Int)] {
let sortedFilters: [(ChatListFilter, Int)]
if let updatedFilterOrder = order {
var updatedFilters: [(ChatListFilter, Int)] = []
for id in updatedFilterOrder {
if let index = filters.firstIndex(where: { $0.0.id == id }) {
updatedFilters.append(filters[index])
}
}
sortedFilters = updatedFilters
} else {
sortedFilters = filters
}
return sortedFilters
}
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], displayTags: Bool, isPremium: Bool, limits: EngineConfiguration.UserLimits, premiumLimits: EngineConfiguration.UserLimits) -> [ChatListFilterPresetListEntry] {
var entries: [ChatListFilterPresetListEntry] = []
entries.append(.screenHeader(presentationData.strings.ChatListFolderSettings_Info))
let filteredSuggestedFilters = suggestedFilters.filter { suggestedFilter in
for (filter, _) in filters {
if case let .filter(_, _, _, data) = filter {
if data == suggestedFilter.data {
return false
}
}
}
return true
}
let actualFilters = filters.filter { filter in
if case .allChats = filter.0 {
return false
}
return true
}
entries.append(.listHeader(presentationData.strings.ChatListFolderSettings_FoldersSection))
entries.append(.addItem(text: presentationData.strings.ChatListFilterList_CreateFolder, isEditing: state.isEditing))
var effectiveDisplayTags: Bool?
if isPremium {
effectiveDisplayTags = displayTags
}
if !filters.isEmpty || suggestedFilters.isEmpty {
var folderCount = 0
for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) {
if case .allChats = filter {
entries.append(.preset(index: PresetIndex(value: entries.count), title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true))
}
if case let .filter(_, title, _, _) = filter {
folderCount += 1
entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true))
}
}
entries.append(.listFooter(presentationData.strings.ChatListFolderSettings_EditFoldersInfo))
}
if !filteredSuggestedFilters.isEmpty && actualFilters.count < limits.maxFoldersCount {
entries.append(.suggestedListHeader(presentationData.strings.ChatListFolderSettings_RecommendedFoldersSection))
for filter in filteredSuggestedFilters {
entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title, label: filter.description, preset: filter.data))
}
if filters.isEmpty {
entries.append(.suggestedAddCustom(presentationData.strings.ChatListFolderSettings_RecommendedNewFolder))
}
}
entries.append(.displayTags(effectiveDisplayTags))
entries.append(.displayTagsFooter)
return entries
}
public enum ChatListFilterPresetListControllerMode {
case `default`
case modal
}
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, dismissed: (() -> Void)? = nil) -> ViewController {
let initialState = ChatListFilterPresetListControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((ChatListFilterPresetListControllerState) -> ChatListFilterPresetListControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
let filtersWithCountsSignal = context.engine.peers.updatedChatListFilters()
|> distinctUntilChanged
|> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in
return .single(filters.map { filter -> (ChatListFilter, Int) in
return (filter, 0)
})
}
let filtersWithCounts = Promise<[(ChatListFilter, Int)]>()
filtersWithCounts.set(filtersWithCountsSignal)
let animateNextShowHideTagsTransition = Atomic<Bool?>(value: nil)
let arguments = ChatListFilterPresetListControllerArguments(context: context,
addSuggestedPressed: { title, data in
let _ = combineLatest(
queue: Queue.mainQueue(),
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
),
filtersWithCounts.get() |> take(1)
).start(next: { result, filters in
let (accountPeer, limits, premiumLimits) = result
let isPremium = accountPeer?.isPremium ?? false
let filters = filters.filter { filter in
if case .allChats = filter.0 {
return false
}
return true
}
let limit = limits.maxFoldersCount
let premiumLimit = premiumLimits.maxFoldersCount
if filters.count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
return true
})
pushControllerImpl?(controller)
return
} else if filters.count >= limit && !isPremium {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
return
}
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
let id = context.engine.peers.generateNewChatListFilterId(filters: filters)
filters.append(.filter(id: id, title: title, emoticon: nil, data: data))
return filters
}
|> deliverOnMainQueue).start(next: { _ in
})
})
}, openPreset: { preset in
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: { _ in }))
}, addNew: {
let _ = combineLatest(
queue: Queue.mainQueue(),
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
),
filtersWithCounts.get() |> take(1)
).start(next: { result, filters in
let (accountPeer, limits, premiumLimits) = result
let isPremium = accountPeer?.isPremium ?? false
let filters = filters.filter { filter in
if case .allChats = filter.0 {
return false
}
return true
}
let limit = limits.maxFoldersCount
let premiumLimit = premiumLimits.maxFoldersCount
if filters.count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
return true
})
pushControllerImpl?(controller)
return
} else if filters.count >= limit && !isPremium {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
return
}
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in }))
})
}, setItemWithRevealedOptions: { preset, fromPreset in
updateState { state in
var state = state
if (preset == nil && fromPreset == state.revealedPreset) || (preset != nil && fromPreset == nil) {
state.revealedPreset = preset
}
return state
}
}, removePreset: { id in
let _ = (context.engine.peers.currentChatListFilters()
|> take(1)
|> deliverOnMainQueue).start(next: { filters in
guard let filter = filters.first(where: { $0.id == id }) else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if case let .filter(_, title, _, data) = filter, data.isShared {
let _ = (combineLatest(
context.engine.data.get(
EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))),
EngineDataMap(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init(id:)))
),
context.engine.peers.getExportedChatFolderLinks(id: id),
context.engine.peers.requestLeaveChatFolderSuggestions(folderId: id)
)
|> deliverOnMainQueue).start(next: { peerData, links, defaultSelectedPeerIds in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let peers = peerData.0
var memberCounts: [EnginePeer.Id: Int] = [:]
for (id, count) in peerData.1 {
if let count {
memberCounts[id] = count
}
}
var hasLinks = false
if let links, !links.isEmpty {
hasLinks = true
}
let confirmDeleteFolder: () -> Void = {
let filteredPeers = peers.compactMap { $0 }.filter { peer in
if case .channel = peer {
return true
} else {
return false
}
}
if filteredPeers.isEmpty {
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
if let index = filters.firstIndex(where: { $0.id == id }) {
filters.remove(at: index)
}
return filters
}
|> deliverOnMainQueue).start()
} else {
let previewScreen = ChatFolderLinkPreviewScreen(
context: context,
subject: .remove(folderId: id, defaultSelectedPeerIds: defaultSelectedPeerIds),
contents: ChatFolderLinkContents(
localFilterId: id,
title: title,
peers: filteredPeers,
alreadyMemberPeerIds: Set(),
memberCounts: memberCounts
)
)
pushControllerImpl?(previewScreen)
}
}
if hasLinks {
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
confirmDeleteFolder()
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {
})
]))
} else {
confirmDeleteFolder()
}
})
} else {
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.ChatList_RemoveFolderConfirmation),
ActionSheetButtonItem(title: presentationData.strings.ChatList_RemoveFolderAction, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
if let index = filters.firstIndex(where: { $0.id == id }) {
filters.remove(at: index)
}
return filters
}
|> deliverOnMainQueue).startStandalone()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
presentControllerImpl?(actionSheet)
}
})
}, updateDisplayTags: { value in
context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value)
}, updateDisplayTagsLocked: {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
})
let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState])
|> map { preferences -> [ChatListFeaturedFilter] in
guard let state = preferences.values[PreferencesKeys.chatListFiltersFeaturedState]?.get(ChatListFiltersFeaturedState.self) else {
return []
}
return state.filters
}
|> distinctUntilChanged
let updatedFilterOrder = Promise<[Int32]?>(nil)
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
let previousDisplayTags = Atomic<Bool?>(value: nil)
let limits = context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
)
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get(),
filtersWithCounts.get(),
preferences,
updatedFilterOrder.get(),
featuredFilters,
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
limits,
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.ChatList.FiltersDisplayTags()
)
)
|> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits, displayTags -> (ItemListControllerState, (ItemListNodeState, Any)) in
let isPremium = peer?.isPremium ?? false
let limits = allLimits.0
let premiumLimits = allLimits.1
let leftNavigationButton: ItemListNavigationButton?
switch mode {
case .default:
leftNavigationButton = nil
case .modal:
leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Close), style: .regular, enabled: true, action: {
dismissImpl?()
})
}
let rightNavigationButton: ItemListNavigationButton?
if state.isEditing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
let _ = (updatedFilterOrder.get()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak updatedFilterOrder] updatedFilterOrderValue in
if let updatedFilterOrderValue = updatedFilterOrderValue {
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var updatedFilters: [ChatListFilter] = []
for id in updatedFilterOrderValue {
if let index = filters.firstIndex(where: { $0.id == id }) {
updatedFilters.append(filters[index])
}
}
for filter in filters {
if !updatedFilters.contains(where: { $0.id == filter.id }) {
updatedFilters.append(filter)
}
}
return updatedFilters
}
|> deliverOnMainQueue).start(next: { _ in
filtersWithCounts.set(filtersWithCountsSignal)
let _ = (filtersWithCounts.get()
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
updatedFilterOrder?.set(.single(nil))
updateState { state in
var state = state
state.isEditing = false
return state
}
})
})
} else {
updateState { state in
var state = state
state.isEditing = false
return state
}
}
})
})
} else if !filtersWithCountsValue.isEmpty {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState { state in
var state = state
state.isEditing = true
return state
}
})
} else {
rightNavigationButton = nil
}
let previousDisplayTagsValue = previousDisplayTags.swap(displayTags)
if let previousDisplayTagsValue, previousDisplayTagsValue != displayTags {
let _ = animateNextShowHideTagsTransition.swap(displayTags)
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
}
var previousOrder: [Int32]?
let controller = ItemListController(context: context, state: signal)
controller.isOpaqueWhenInOverlay = true
controller.blocksBackgroundWhenInOverlay = true
switch mode {
case .default:
controller.navigationPresentation = .default
case .modal:
controller.navigationPresentation = .modal
}
controller.didDisappear = { _ in
dismissed?()
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ChatListFilterPresetListEntry]) -> Signal<Bool, NoError> in
let fromEntry = entries[fromIndex]
guard case let .preset(_, _, _, fromPreset, _, _, _, _, _, _) = fromEntry else {
return .single(false)
}
var referenceFilter: ChatListFilter?
var beforeAll = false
var afterAll = false
if toIndex < entries.count {
switch entries[toIndex] {
case let .preset(_, _, _, preset, _, _, _, _, _, _):
referenceFilter = preset
default:
if entries[toIndex] < fromEntry {
beforeAll = true
} else {
afterAll = true
}
}
} else {
afterAll = true
}
return combineLatest(
updatedFilterOrder.get() |> take(1),
filtersWithCounts.get() |> take(1)
)
|> mapToSignal { updatedFilterOrderValue, filtersWithCountsValue -> Signal<Bool, NoError> in
var filters = filtersWithAppliedOrder(filters: filtersWithCountsValue, order: updatedFilterOrderValue).map { $0.0 }
let initialOrder = filters.map { $0.id }
if let index = filters.firstIndex(where: { $0.id == fromPreset.id }) {
filters.remove(at: index)
}
if let referenceFilter = referenceFilter {
var inserted = false
for i in 0 ..< filters.count {
if filters[i].id == referenceFilter.id {
if fromIndex < toIndex {
filters.insert(fromPreset, at: i + 1)
} else {
filters.insert(fromPreset, at: i)
}
inserted = true
break
}
}
if !inserted {
filters.append(fromPreset)
}
} else if beforeAll {
filters.insert(fromPreset, at: 0)
} else if afterAll {
filters.append(fromPreset)
}
let updatedOrder = filters.map { $0.id }
if initialOrder != updatedOrder {
updatedFilterOrder.set(.single(updatedOrder))
return .single(true)
} else {
return .single(false)
}
}
})
controller.setReorderCompleted({ (entries: [ChatListFilterPresetListEntry]) -> Void in
let _ = (combineLatest(
updatedFilterOrder.get() |> take(1),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
)
|> deliverOnMainQueue).start(next: { order, peer in
let isPremium = peer?.isPremium ?? false
if !isPremium, let order = order, order.first != 0 {
updatedFilterOrder.set(.single(previousOrder))
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: nil, text: presentationData.strings.ChatListFolderSettings_SubscribeToMoveAll, customUndoText: presentationData.strings.ChatListFolderSettings_SubscribeToMoveAllAction, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if case .undo = action {
pushControllerImpl?(PremiumIntroScreen(context: context, source: .folders))
}
return false })
)
} else {
previousOrder = order
}
})
})
controller.afterTransactionCompleted = { [weak controller] in
guard let toggleDirection = animateNextShowHideTagsTransition.swap(nil) else {
return
}
guard let controller else {
return
}
var presetItemNodes: [ChatListFilterPresetListItemNode] = []
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatListFilterPresetListItemNode {
presetItemNodes.append(itemNode)
}
}
var delay: Double = 0.0
for itemNode in presetItemNodes.reversed() {
if toggleDirection {
itemNode.animateTagColorIn(delay: delay)
}
delay += 0.02
}
}
return controller
}
@@ -0,0 +1,636 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import TelegramUIPreferences
import AccountContext
import TextNodeWithEntities
struct ChatListFilterPresetListItemEditing: Equatable {
let editable: Bool
let editing: Bool
let revealed: Bool
}
final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let preset: ChatListFilter
let title: ChatFolderTitle
let label: String
let tagColor: UIColor?
let editing: ChatListFilterPresetListItemEditing
let canBeReordered: Bool
let canBeDeleted: Bool
let isAllChats: Bool
let isDisabled: Bool
let sectionId: ItemListSectionId
let action: () -> Void
let setItemWithRevealedOptions: (Int32?, Int32?) -> Void
let remove: () -> Void
init(
context: AccountContext,
presentationData: ItemListPresentationData,
systemStyle: ItemListSystemStyle,
preset: ChatListFilter,
title: ChatFolderTitle,
label: String,
tagColor: UIColor?,
editing: ChatListFilterPresetListItemEditing,
canBeReordered: Bool,
canBeDeleted: Bool,
isAllChats: Bool,
isDisabled: Bool,
sectionId: ItemListSectionId,
action: @escaping () -> Void,
setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void,
remove: @escaping () -> Void
) {
self.context = context
self.presentationData = presentationData
self.systemStyle = systemStyle
self.preset = preset
self.title = title
self.label = label
self.tagColor = tagColor
self.editing = editing
self.canBeReordered = canBeReordered
self.canBeDeleted = canBeDeleted
self.isAllChats = isAllChats
self.isDisabled = isDisabled
self.sectionId = sectionId
self.action = action
self.setItemWithRevealedOptions = setItemWithRevealedOptions
self.remove = remove
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatListFilterPresetListItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ChatListFilterPresetListItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animated)
})
}
}
}
}
}
var selectable: Bool {
return !self.isAllChats
}
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action()
}
}
private let titleFont = Font.regular(17.0)
final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
override var controlsContainer: ASDisplayNode {
return self.containerNode
}
private let titleNode: TextNodeWithEntities
private let labelNode: TextNode
private let arrowNode: ASImageNode
private let sharedIconNode: ASImageNode
private var tagIconView: UIImageView?
private let activateArea: AccessibilityAreaNode
private var editableControlNode: ItemListEditableControlNode?
private var reorderControlNode: ItemListEditableReorderControlNode?
private var item: ChatListFilterPresetListItem?
private var layoutParams: ListViewItemLayoutParams?
override var canBeSelected: Bool {
if self.editableControlNode != nil {
return false
}
return true
}
override var visibility: ListViewItemNodeVisibility {
didSet {
if self.visibility != oldValue {
let enableAnimations = self.item?.title.enableAnimations ?? true
self.titleNode.visibilityRect = (self.visibility == ListViewItemNodeVisibility.none || !enableAnimations) ? CGRect.zero : CGRect.infinite
}
}
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.containerNode = ASDisplayNode()
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.titleNode = TextNodeWithEntities()
self.titleNode.textNode.isUserInteractionEnabled = false
self.titleNode.textNode.contentMode = .left
self.titleNode.textNode.contentsScale = UIScreen.main.scale
self.titleNode.resetEmojiToFirstFrameAutomatically = true
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.isLayerBacked = true
self.sharedIconNode = ASImageNode()
self.sharedIconNode.displayWithoutProcessing = true
self.sharedIconNode.displaysAsynchronously = false
self.sharedIconNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.titleNode.textNode)
self.containerNode.addSubnode(self.labelNode)
self.containerNode.addSubnode(self.arrowNode)
self.containerNode.addSubnode(self.sharedIconNode)
self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in
self?.item?.action()
return true
}
}
func asyncLayout() -> (_ item: ChatListFilterPresetListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
var updateArrowImage: UIImage?
var updatedSharedIconImage: UIImage?
if currentItem?.presentationData.theme !== item.presentationData.theme || currentItem?.isDisabled != item.isDisabled {
updatedTheme = item.presentationData.theme
if item.isDisabled {
updateArrowImage = PresentationResourcesItemList.disclosureLockedImage(item.presentationData.theme)
} else {
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
}
updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/SharedFolderListIcon"), color: item.presentationData.theme.list.disclosureArrowColor)
}
let peerRevealOptions: [ItemListRevealOption]
if item.editing.editable && item.canBeDeleted {
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]
} else {
peerRevealOptions = []
}
let titleAttributedString = NSMutableAttributedString()
if item.isAllChats {
titleAttributedString.append(NSAttributedString(string: item.presentationData.strings.ChatList_FolderAllChats, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor))
} else {
titleAttributedString.append(item.title.attributedString(font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor))
}
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
var editingOffset: CGFloat = 0.0
var reorderInset: CGFloat = 0.0
if item.editing.editing {
let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
if item.canBeReordered {
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = reorderSizeAndApply
reorderInset = reorderSizeAndApply.0
}
}
let leftInset: CGFloat = 16.0 + params.leftInset
let rightInset: CGFloat = params.rightInset + max(reorderInset, 55.0)
let rightArrowInset: CGFloat = 34.0 + params.rightInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let labelConstrain: CGFloat = params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: titleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset * 2.0)
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.activateArea.accessibilityLabel = "\(titleAttributedString.string))"
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
strongSelf.reorderControlNode = reorderControlNode
strongSelf.controlsContainer.addSubnode(reorderControlNode)
reorderControlNode.alpha = 0.0
transition.updateAlpha(node: reorderControlNode, alpha: 1.0)
}
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height))
strongSelf.reorderControlNode?.frame = reorderControlFrame
} else if let reorderControlNode = strongSelf.reorderControlNode {
strongSelf.reorderControlNode = nil
transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in
reorderControlNode?.removeFromSupernode()
})
}
if let editableControlSizeAndApply = editableControlSizeAndApply {
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height))
if strongSelf.editableControlNode == nil {
let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height)
editableControlNode.tapped = {
if let strongSelf = self {
strongSelf.setRevealOptionsOpened(true, animated: true)
strongSelf.revealOptionsInteractivelyOpened()
}
}
strongSelf.editableControlNode = editableControlNode
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.containerNode)
editableControlNode.frame = editableControlFrame
transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY))
editableControlNode.alpha = 0.0
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
} else {
strongSelf.editableControlNode?.frame = editableControlFrame
}
strongSelf.editableControlNode?.isHidden = !item.editing.editable
} else if let editableControlNode = strongSelf.editableControlNode {
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = -editableControlFrame.size.width
strongSelf.editableControlNode = nil
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
editableControlNode?.removeFromSupernode()
})
}
strongSelf.editableControlNode?.isHidden = !item.canBeDeleted
let _ = titleApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: true
))
let _ = labelApply()
let enableAnimations = item.title.enableAnimations
strongSelf.titleNode.visibilityRect = (strongSelf.visibility == ListViewItemNodeVisibility.none || !enableAnimations) ? CGRect.zero : CGRect.infinite
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.addSubnode(strongSelf.maskNode)
}
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
strongSelf.bottomStripeNode.isHidden = false
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, glass: item.systemStyle == .glass) : 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.containerNode.frame = CGRect(origin: CGPoint(), size: strongSelf.backgroundNode.frame.size)
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 - params.rightInset - separatorRightInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size))
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.arrowNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.sharedIconNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
if let updateArrowImage = updateArrowImage {
strongSelf.arrowNode.image = updateArrowImage
}
if let updatedSharedIconImage {
strongSelf.sharedIconNode.image = updatedSharedIconImage
}
if let arrowImage = strongSelf.arrowNode.image {
var rightArrowInset = 0.0
if item.isDisabled == true {
rightArrowInset -= 3.0
}
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width + rightArrowInset + revealOffset, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
}
strongSelf.arrowNode.isHidden = item.isAllChats
if let sharedIconImage = strongSelf.sharedIconNode.image {
var sharedIconFrame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX + 2.0 - sharedIconImage.size.width, y: floorToScreenPixels((layout.contentSize.height - sharedIconImage.size.height) / 2.0) + 1.0), size: sharedIconImage.size)
if item.tagColor != nil {
sharedIconFrame.origin.x -= 34.0
}
if strongSelf.sharedIconNode.bounds.isEmpty {
strongSelf.sharedIconNode.frame = sharedIconFrame
} else {
transition.updateFrame(node: strongSelf.sharedIconNode, frame: sharedIconFrame)
}
}
var isShared = false
if case let .filter(_, _, _, data) = item.preset, data.isShared {
isShared = true
}
strongSelf.sharedIconNode.isHidden = !isShared
if let tagColor = item.tagColor {
let tagIconView: UIImageView
var tagIconTransition = transition
if let current = strongSelf.tagIconView {
tagIconView = current
} else {
tagIconTransition = .immediate
tagIconView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 24.0, color: .white)?.withRenderingMode(.alwaysTemplate))
strongSelf.tagIconView = tagIconView
strongSelf.containerNode.view.addSubview(tagIconView)
}
tagIconView.tintColor = tagColor
let tagIconFrame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX - 2.0 - 24.0, y: floorToScreenPixels((layout.contentSize.height - 24.0) / 2.0)), size: CGSize(width: 24.0, height: 24.0))
tagIconTransition.updateAlpha(layer: tagIconView.layer, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
tagIconTransition.updateFrame(view: tagIconView, frame: tagIconFrame)
} else {
if let tagIconView = strongSelf.tagIconView {
strongSelf.tagIconView = nil
transition.updateAlpha(layer: tagIconView.layer, alpha: 0.0, completion: { [weak tagIconView] _ in
tagIconView?.removeFromSuperview()
})
transition.updateTransformScale(layer: tagIconView.layer, scale: 0.001)
}
}
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated)
}
})
}
}
func animateTagColorIn(delay: Double) {
if let tagIconView = self.tagIconView {
tagIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12, delay: delay)
tagIconView.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: delay)
}
}
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, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
guard let params = self.layoutParams else {
return
}
let leftInset: CGFloat = 16.0 + params.leftInset
var rightArrowInset: CGFloat = 34.0 + params.rightInset
if self.item?.isDisabled == true {
rightArrowInset -= 3.0
}
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.textNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.textNode.frame.minY), size: self.titleNode.textNode.bounds.size))
var labelFrame = self.labelNode.frame
labelFrame.origin.x = params.width - rightArrowInset - labelFrame.width + revealOffset
transition.updateFrame(node: self.labelNode, frame: labelFrame)
var arrowFrame = self.arrowNode.frame
arrowFrame.origin.x = params.width - params.rightInset - 7.0 - arrowFrame.width + revealOffset
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
var sharedIconFrame = self.sharedIconNode.frame
sharedIconFrame.origin.x = arrowFrame.minX + 2.0 - sharedIconFrame.width
if self.item?.tagColor != nil {
sharedIconFrame.origin.x -= 34.0
}
transition.updateFrame(node: self.sharedIconNode, frame: sharedIconFrame)
if let tagIconView = self.tagIconView {
var tagIconFrame = tagIconView.frame
tagIconFrame.origin.x = arrowFrame.minX - 2.0 - tagIconFrame.width
transition.updateFrame(view: tagIconView, frame: tagIconFrame)
}
}
override func revealOptionsInteractivelyOpened() {
if let item = self.item {
item.setItemWithRevealedOptions(item.preset.id, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let item = self.item {
item.setItemWithRevealedOptions(nil, item.preset.id)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let item = self.item {
item.remove()
}
}
override func isReorderable(at point: CGPoint) -> Bool {
if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions {
return true
}
return false
}
}
@@ -0,0 +1,393 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
public class ChatListFilterPresetListSuggestedItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let title: String
let label: String
public let sectionId: ItemListSectionId
let style: ItemListStyle
let installAction: (() -> Void)?
public let tag: ItemListItemTag?
public init(
presentationData: ItemListPresentationData,
systemStyle: ItemListSystemStyle,
title: String,
label: String,
sectionId: ItemListSectionId,
style: ItemListStyle,
installAction: (() -> Void)?,
tag: ItemListItemTag? = nil
) {
self.presentationData = presentationData
self.systemStyle = systemStyle
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 separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
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.ChatListFolderSettings_AddRecommended, 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 + 14.0 * 2.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
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 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, params)
}
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
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : 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 - params.rightInset - separatorRightInset, height: separatorHeight))
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), 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, options: ListViewItemAnimationOptions) {
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)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,366 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ActivityIndicator
import ItemListUI
import AccountContext
import TelegramCore
import TextNodeWithEntities
public class ChatListFilterTagSectionHeaderItem: ListViewItem, ItemListItem {
public struct BadgeStyle: Equatable {
public var background: UIColor
public var foreground: UIColor
public init(background: UIColor, foreground: UIColor) {
self.background = background
self.foreground = foreground
}
}
let context: AccountContext
let presentationData: ItemListPresentationData
let text: String
let badge: ChatFolderTitle?
let badgeStyle: BadgeStyle?
let multiline: Bool
let activityIndicator: ItemListSectionHeaderActivityIndicator
let accessoryText: ItemListSectionHeaderAccessoryText?
let actionText: String?
let action: (() -> Void)?
public let sectionId: ItemListSectionId
public let isAlwaysPlain: Bool = true
public init(context: AccountContext, presentationData: ItemListPresentationData, text: String, badge: ChatFolderTitle? = nil, badgeStyle: BadgeStyle? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId) {
self.context = context
self.presentationData = presentationData
self.text = text
self.badge = badge
self.badgeStyle = badgeStyle
self.multiline = multiline
self.activityIndicator = activityIndicator
self.accessoryText = accessoryText
self.actionText = actionText
self.action = action
self.sectionId = sectionId
}
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 = ChatListFilterTagSectionHeaderItemNode()
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 {
guard let nodeValue = node() as? ChatListFilterTagSectionHeaderItemNode 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()
})
}
}
}
}
}
public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode {
private var item: ChatListFilterTagSectionHeaderItem?
private let titleNode: TextNode
private var badgeBackgroundLayer: SimpleLayer?
private var badgeTextNode: TextNodeWithEntities?
private let accessoryTextNode: TextNode
private var accessoryImageNode: ASImageNode?
private var activityIndicator: ActivityIndicator?
private var actionNode: TextNode?
private var actionButtonNode: HighlightableButtonNode?
private let activateArea: AccessibilityAreaNode
public init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.accessoryTextNode = TextNode()
self.accessoryTextNode.isUserInteractionEnabled = false
self.accessoryTextNode.contentMode = .left
self.accessoryTextNode.contentsScale = UIScreen.main.scale
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = [.staticText, .header]
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.accessoryTextNode)
self.addSubnode(self.activateArea)
}
public func asyncLayout() -> (_ item: ChatListFilterTagSectionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeActionLayout = TextNode.asyncLayout(self.actionNode)
let makeBadgeTextLayout = TextNodeWithEntities.asyncLayout(self.badgeTextNode)
let makeAccessoryTextLayout = TextNode.asyncLayout(self.accessoryTextNode)
let previousItem = self.item
return { item, params, neighbors in
let leftInset: CGFloat = 15.0 + params.leftInset
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
var badgeLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
if let badge = item.badge {
if item.badgeStyle != nil {
let badgeFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize * 12.0 / 13.0)
badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badge.attributedString(font: badgeFont, textColor: item.badgeStyle?.foreground ?? item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
} else {
let badgeFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize * 11.0 / 13.0)
badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badge.attributedString(font: badgeFont, textColor: item.badgeStyle?.foreground ?? item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
}
}
let badgeSpacing: CGFloat = 6.0
var textRightInset: CGFloat = 20.0
if let badgeLayoutAndApply {
textRightInset += badgeLayoutAndApply.0.size.width + badgeSpacing
}
var actionLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let actionText = item.actionText {
let actionLayoutAndApplyValue = makeActionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: actionText, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
actionLayoutAndApply = actionLayoutAndApplyValue
textRightInset += actionLayoutAndApplyValue.0.size.width + 2.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var accessoryTextString: NSAttributedString?
var accessoryIcon: UIImage?
if let accessoryText = item.accessoryText {
let color: UIColor
switch accessoryText.color {
case .generic:
color = item.presentationData.theme.list.sectionHeaderTextColor
case .destructive:
color = item.presentationData.theme.list.freeTextErrorColor
}
accessoryTextString = NSAttributedString(string: accessoryText.value, font: titleFont, textColor: color)
accessoryIcon = accessoryText.icon
}
let (accessoryLayout, accessoryApply) = makeAccessoryTextLayout(TextNodeLayoutArguments(attributedString: accessoryTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
var insets = UIEdgeInsets()
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 13.0)
switch neighbors.top {
case .none:
insets.top += 24.0
case .otherSection:
insets.top += 28.0
default:
break
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let _ = titleApply()
let _ = accessoryApply()
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.text
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: titleLayout.size)
if let (actionLayout, actionApply) = actionLayoutAndApply {
let actionButtonNode: HighlightableButtonNode
if let current = strongSelf.actionButtonNode {
actionButtonNode = current
} else {
actionButtonNode = HighlightableButtonNode()
strongSelf.actionButtonNode = actionButtonNode
actionButtonNode.hitTestSlop = UIEdgeInsets(top: -4.0, left: -4.0, bottom: -4.0, right: -4.0)
strongSelf.addSubnode(actionButtonNode)
actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed), forControlEvents: .touchUpInside)
}
let actionNode = actionApply()
if strongSelf.actionNode !== actionNode {
strongSelf.actionNode?.removeFromSupernode()
strongSelf.actionNode = actionNode
actionButtonNode.addSubnode(actionNode)
}
actionButtonNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - actionLayout.size.width, y: 7.0), size: actionLayout.size)
actionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionLayout.size)
} else {
if let actionNode = strongSelf.actionNode {
strongSelf.actionNode = nil
actionNode.removeFromSupernode()
}
if let actionButtonNode = strongSelf.actionButtonNode {
strongSelf.actionButtonNode = nil
actionButtonNode.removeFromSupernode()
}
}
if let badgeLayoutAndApply {
let badgeTextNode = badgeLayoutAndApply.1(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: true,
emojiOffset: CGPoint(x: 0.0, y: -1.0)
))
let badgeSideInset: CGFloat = 4.0
let badgeBackgroundSize: CGSize
if item.badgeStyle != nil {
badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0)
} else {
badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0)
}
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize)
let badgeBackgroundLayer: SimpleLayer
if let current = strongSelf.badgeBackgroundLayer {
badgeBackgroundLayer = current
} else {
badgeBackgroundLayer = SimpleLayer()
strongSelf.badgeBackgroundLayer = badgeBackgroundLayer
strongSelf.layer.addSublayer(badgeBackgroundLayer)
}
if strongSelf.badgeTextNode !== badgeTextNode {
strongSelf.badgeTextNode?.textNode.removeFromSupernode()
strongSelf.badgeTextNode = badgeTextNode
badgeTextNode.resetEmojiToFirstFrameAutomatically = true
strongSelf.addSubnode(badgeTextNode.textNode)
}
badgeBackgroundLayer.frame = badgeBackgroundFrame
badgeBackgroundLayer.backgroundColor = item.badgeStyle?.background.cgColor ?? item.presentationData.theme.list.itemCheckColors.fillColor.cgColor
badgeBackgroundLayer.cornerRadius = 5.0
badgeTextNode.textNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeLayoutAndApply.0.size.width) * 0.5), y: badgeBackgroundFrame.minY + 1.0 + floorToScreenPixels((badgeBackgroundFrame.height - badgeLayoutAndApply.0.size.height) * 0.5)), size: badgeLayoutAndApply.0.size)
if item.badge?.enableAnimations ?? false {
badgeTextNode.visibilityRect = .infinite
} else {
badgeTextNode.visibilityRect = CGRect()
}
} else {
if let badgeTextNode = strongSelf.badgeTextNode {
strongSelf.badgeTextNode = nil
badgeTextNode.textNode.removeFromSupernode()
}
if let badgeBackgroundLayer = strongSelf.badgeBackgroundLayer {
strongSelf.badgeBackgroundLayer = nil
badgeBackgroundLayer.removeFromSuperlayer()
}
}
var accessoryTextOffset: CGFloat = 0.0
if let accessoryIcon = accessoryIcon {
accessoryTextOffset += accessoryIcon.size.width + 3.0
}
strongSelf.accessoryTextNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryLayout.size.width - accessoryTextOffset, y: 7.0), size: accessoryLayout.size)
if let accessoryIcon = accessoryIcon {
let accessoryImageNode: ASImageNode
if let currentAccessoryImageNode = strongSelf.accessoryImageNode {
accessoryImageNode = currentAccessoryImageNode
} else {
accessoryImageNode = ASImageNode()
accessoryImageNode.displaysAsynchronously = false
accessoryImageNode.displayWithoutProcessing = true
strongSelf.addSubnode(accessoryImageNode)
strongSelf.accessoryImageNode = accessoryImageNode
}
accessoryImageNode.image = accessoryIcon
accessoryImageNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryIcon.size.width, y: 7.0), size: accessoryIcon.size)
} else if let accessoryImageNode = strongSelf.accessoryImageNode {
accessoryImageNode.removeFromSupernode()
strongSelf.accessoryImageNode = nil
}
if previousItem?.activityIndicator != item.activityIndicator {
if item.activityIndicator.hasActivity {
let activityIndicator: ActivityIndicator
if let currentActivityIndicator = strongSelf.activityIndicator {
activityIndicator = currentActivityIndicator
} else {
activityIndicator = ActivityIndicator(type: .custom(item.presentationData.theme.list.sectionHeaderTextColor, 18.0, 1.0, false))
strongSelf.addSubnode(activityIndicator)
strongSelf.activityIndicator = activityIndicator
}
activityIndicator.isHidden = false
if previousItem != nil {
activityIndicator.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
}
} else if let activityIndicator = strongSelf.activityIndicator {
activityIndicator.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { finished in
if finished {
activityIndicator.isHidden = true
}
})
}
}
var activityIndicatorOrigin: CGPoint?
switch item.activityIndicator {
case .left:
activityIndicatorOrigin = CGPoint(x: strongSelf.titleNode.frame.maxX + 6.0, y: 7.0 - UIScreenPixel)
case .right:
activityIndicatorOrigin = CGPoint(x: params.width - leftInset - 18.0, y: 7.0 - UIScreenPixel)
default:
break
}
if let activityIndicatorOrigin = activityIndicatorOrigin {
strongSelf.activityIndicator?.frame = CGRect(origin: activityIndicatorOrigin, size: CGSize(width: 18.0, height: 18.0))
}
}
})
}
}
@objc private func actionButtonPressed() {
self.item?.action?()
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
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,187 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ChatListSearchRecentPeersNode
import ContextUI
import AccountContext
class ChatListRecentPeersListItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let context: AccountContext
let peers: [EnginePeer]
let peerSelected: (EnginePeer) -> Void
let peerContextAction: (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void
let header: ListViewItemHeader?
init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, peers: [EnginePeer], peerSelected: @escaping (EnginePeer) -> Void, peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void) {
self.theme = theme
self.strings = strings
self.context = context
self.peers = peers
self.peerSelected = peerSelected
self.peerContextAction = peerContextAction
self.header = nil
}
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 = ChatListRecentPeersListItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem != nil)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, {
return (nil, { _ in nodeApply(synchronousLoads) })
})
}
}
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? ChatListRecentPeersListItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem != nil)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(false)
})
}
}
}
}
}
}
class ChatListRecentPeersListItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private var peersNode: ChatListSearchRecentPeersNode?
private var item: ChatListRecentPeersListItem?
private let ready = Promise<Bool>()
public var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply(false)
}
}
func asyncLayout() -> (_ item: ChatListRecentPeersListItem, _ params: ListViewItemLayoutParams, _ last: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let currentItem = self.item
return { [weak self] item, params, last in
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 96.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
return (nodeLayout, { [weak self] synchronousLoads in
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor
}
let peersNode: ChatListSearchRecentPeersNode
if let currentPeersNode = strongSelf.peersNode {
peersNode = currentPeersNode
peersNode.updateThemeAndStrings(theme: item.theme, strings: item.strings)
} else {
peersNode = ChatListSearchRecentPeersNode(
accountPeerId: item.context.account.peerId,
postbox: item.context.account.postbox,
network: item.context.account.network,
energyUsageSettings: item.context.sharedContext.energyUsageSettings,
contentSettings: item.context.currentContentSettings.with { $0 },
animationCache: item.context.animationCache,
animationRenderer: item.context.animationRenderer,
resolveInlineStickers: item.context.engine.stickers.resolveInlineStickers,
theme: item.theme,
mode: .list(compact: false),
strings: item.strings,
peerSelected: { peer in
self?.item?.peerSelected(peer)
},
peerContextAction: { peer, node, gesture, location in
self?.item?.peerContextAction(peer, node, gesture, location)
},
isPeerSelected: { _ in
return false
}
)
strongSelf.ready.set(peersNode.isReady)
strongSelf.peersNode = peersNode
strongSelf.addSubnode(peersNode)
}
peersNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
peersNode.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
let separatorHeight = UIScreenPixel
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight))
strongSelf.separatorNode.isHidden = true
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
override public func headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
func viewAndPeerAtPoint(_ point: CGPoint) -> (UIView, EnginePeer.Id)? {
if let peersNode = self.peersNode {
let adjustedLocation = self.convert(point, to: peersNode)
if let result = peersNode.viewAndPeerAtPoint(adjustedLocation) {
return result
}
}
return nil
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,498 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
private final class ItemNode: ASDisplayNode {
private let pressed: () -> Void
private let iconNode: ASImageNode
private let titleNode: ImmediateTextNode
private let titleActiveNode: ImmediateTextNode
private var titleBadgeView: UIImageView?
private let buttonNode: HighlightTrackingButtonNode
private var selectionFraction: CGFloat = 0.0
private var theme: PresentationTheme?
init(pressed: @escaping () -> Void) {
self.pressed = pressed
let titleInset: CGFloat = 4.0
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
self.titleActiveNode = ImmediateTextNode()
self.titleActiveNode.displaysAsynchronously = false
self.titleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
self.titleActiveNode.alpha = 0.0
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.titleActiveNode)
self.addSubnode(self.iconNode)
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.iconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.iconNode.alpha = 0.4
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleNode.alpha = 0.4
} else {
strongSelf.iconNode.alpha = 1.0
strongSelf.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.titleNode.alpha = 1.0
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
@objc private func buttonPressed() {
self.pressed()
}
func update(type: ChatListSearchFilter, displayNewBadge: Bool, presentationData: PresentationData, selectionFraction: CGFloat, transition: ContainedViewLayoutTransition) {
self.selectionFraction = selectionFraction
let title: String
var titleBadge: String?
let icon: UIImage?
let color = presentationData.theme.list.itemSecondaryTextColor
switch type {
case .chats:
title = presentationData.strings.ChatList_Search_FilterChats
icon = nil
case .topics:
title = presentationData.strings.ChatList_Search_FilterChats
icon = nil
case .channels:
title = presentationData.strings.ChatList_Search_FilterChannels
icon = nil
case .apps:
title = presentationData.strings.ChatList_Search_FilterApps
icon = nil
case .globalPosts:
title = presentationData.strings.ChatList_Search_FilterGlobalPosts
if displayNewBadge {
titleBadge = presentationData.strings.ChatList_ContextMenuBadgeNew
}
icon = nil
case .media:
title = presentationData.strings.ChatList_Search_FilterMedia
icon = nil
case .downloads:
title = presentationData.strings.ChatList_Search_FilterDownloads
icon = nil
case .links:
title = presentationData.strings.ChatList_Search_FilterLinks
icon = nil
case .files:
title = presentationData.strings.ChatList_Search_FilterFiles
icon = nil
case .music:
title = presentationData.strings.ChatList_Search_FilterMusic
icon = nil
case .voice:
title = presentationData.strings.ChatList_Search_FilterVoice
icon = nil
case .instantVideo:
title = presentationData.strings.ChatList_Search_FilterVoice
icon = nil
case .publicPosts:
title = presentationData.strings.ChatList_Search_FilterPublicPosts
icon = nil
case let .peer(peerId, isGroup, displayTitle, _):
title = displayTitle
let image: UIImage?
if isGroup {
image = UIImage(bundleImageName: "Chat List/Search/Group")
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
image = UIImage(bundleImageName: "Chat List/Search/Channel")
} else {
image = UIImage(bundleImageName: "Chat List/Search/User")
}
icon = generateTintedImage(image: image, color: color)
case let .date(_, _, displayTitle):
title = displayTitle
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Calendar"), color: color)
}
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: color)
self.titleActiveNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
if let titleBadge {
let titleBadgeView: UIImageView
if let current = self.titleBadgeView {
titleBadgeView = current
} else {
titleBadgeView = UIImageView()
self.titleBadgeView = titleBadgeView
self.view.addSubview(titleBadgeView)
let labelText = NSAttributedString(string: titleBadge, font: Font.medium(11.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
let labelBounds = labelText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height))
let badgeSize = CGSize(width: labelSize.width + 8.0, height: labelSize.height + 2.0 + 1.0)
titleBadgeView.image = generateImage(badgeSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let rect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height - UIScreenPixel * 2.0))
context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 5.0).cgPath)
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.fillPath()
UIGraphicsPushContext(context)
labelText.draw(at: CGPoint(x: 4.0, y: 1.0 + UIScreenPixel))
UIGraphicsPopContext()
})
}
} else if let titleBadgeView = self.titleBadgeView {
self.titleBadgeView = nil
titleBadgeView.removeFromSuperview()
}
let selectionAlpha: CGFloat = selectionFraction * selectionFraction
let deselectionAlpha: CGFloat = 1.0// - selectionFraction
transition.updateAlpha(node: self.titleNode, alpha: deselectionAlpha)
transition.updateAlpha(node: self.titleActiveNode, alpha: selectionAlpha)
self.buttonNode.accessibilityLabel = title
if selectionFraction == 1.0 {
self.buttonNode.accessibilityTraits = [.button, .selected]
} else {
self.buttonNode.accessibilityTraits = [.button]
}
if self.theme !== presentationData.theme {
self.theme = presentationData.theme
self.iconNode.image = icon
}
}
func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
var iconInset: CGFloat = 0.0
if let image = self.iconNode.image {
iconInset = 22.0
self.iconNode.frame = CGRect(x: 0.0, y: floorToScreenPixels((height - image.size.height) / 2.0), width: image.size.width, height: image.size.height)
}
let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude))
let _ = self.titleActiveNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude))
let titleFrame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left + iconInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
self.titleNode.frame = titleFrame
self.titleActiveNode.frame = titleFrame
var width = titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + iconInset
if let titleBadgeView = self.titleBadgeView, let image = titleBadgeView.image {
width += 4.0 + image.size.width
titleBadgeView.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) + 1.0), size: image.size)
}
return width
}
func updateArea(size: CGSize, sideInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height))
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -sideInset, bottom: 0.0, right: -sideInset)
}
}
enum ChatListSearchFilterEntryId: Hashable {
case filter(Int64)
}
enum ChatListSearchFilterEntry: Equatable {
case filter(ChatListSearchFilter)
var id: ChatListSearchFilterEntryId {
switch self {
case let .filter(filter):
return .filter(filter.id)
}
}
}
final class ChatListSearchFiltersContainerNode: ASDisplayNode {
private let scrollNode: ASScrollNode
private let selectedLineNode: ASImageNode
private var itemNodes: [ChatListSearchFilterEntryId: ItemNode] = [:]
var filterPressed: ((ChatListSearchFilter) -> Void)?
private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], selectedFilter: ChatListSearchFilterEntryId?, transitionFraction: CGFloat, presentationData: PresentationData)?
private var previousSelectedAbsFrame: CGRect?
private var previousSelectedFrame: CGRect?
override init() {
self.scrollNode = ASScrollNode()
self.selectedLineNode = ASImageNode()
self.selectedLineNode.displaysAsynchronously = false
self.selectedLineNode.displayWithoutProcessing = true
super.init()
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.selectedLineNode)
}
func cancelAnimations() {
self.scrollNode.layer.removeAllAnimations()
}
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], displayGlobalPostsNewBadge: Bool, selectedFilter: ChatListSearchFilterEntryId?, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) {
let isFirstTime = self.currentParams == nil
let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : proposedTransition
var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter
let previousScrollBounds = self.scrollNode.bounds
let previousContentWidth = self.scrollNode.view.contentSize.width
if self.currentParams?.presentationData.theme !== presentationData.theme {
//self.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
self.selectedLineNode.image = generateImage(CGSize(width: 5.0, height: 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 1.0)))
context.fill(CGRect(x: 0.0, y: 2.0, width: size.width, height: 2.0))
})?.stretchableImage(withLeftCapWidth: 2, topCapHeight: 2)
}
self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, transitionFraction: transitionFraction, presentationData: presentationData)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
var hasSelection = false
for i in 0 ..< filters.count {
let filter = filters[i]
if case let .filter(type) = filter {
let itemNode: ItemNode
var itemNodeTransition = transition
if let current = self.itemNodes[filter.id] {
itemNode = current
} else {
itemNodeTransition = .immediate
itemNode = ItemNode(pressed: { [weak self] in
self?.filterPressed?(type)
})
self.itemNodes[filter.id] = itemNode
}
let selectionFraction: CGFloat
if selectedFilter == filter.id {
selectionFraction = 1.0 - abs(transitionFraction)
hasSelection = true
} else if i != 0 && selectedFilter == filters[i - 1].id {
selectionFraction = max(0.0, -transitionFraction)
} else if i != filters.count - 1 && selectedFilter == filters[i + 1].id {
selectionFraction = max(0.0, transitionFraction)
} else {
selectionFraction = 0.0
}
var displayNewBadge = false
if case .globalPosts = type {
displayNewBadge = displayGlobalPostsNewBadge
}
itemNode.update(type: type, displayNewBadge: displayNewBadge, presentationData: presentationData, selectionFraction: selectionFraction, transition: itemNodeTransition)
}
}
var updated = false
var removeKeys: [ChatListSearchFilterEntryId] = []
for (id, _) in self.itemNodes {
if !filters.contains(where: { $0.id == id }) {
removeKeys.append(id)
updated = true
}
}
for id in removeKeys {
if let itemNode = self.itemNodes.removeValue(forKey: id) {
transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
transition.updateTransformScale(node: itemNode, scale: 0.1)
}
}
var tabSizes: [(ChatListSearchFilterEntryId, CGSize, ItemNode, Bool)] = []
var totalRawTabSize: CGFloat = 0.0
var selectionFrames: [CGRect] = []
for filter in filters {
guard let itemNode = self.itemNodes[filter.id] else {
continue
}
let wasAdded = itemNode.supernode == nil
var itemNodeTransition = transition
if wasAdded {
itemNodeTransition = .immediate
self.scrollNode.addSubnode(itemNode)
}
let paneNodeWidth = itemNode.updateLayout(height: size.height, transition: itemNodeTransition)
let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height)
tabSizes.append((filter.id, paneNodeSize, itemNode, wasAdded))
totalRawTabSize += paneNodeSize.width
}
let minSpacing: CGFloat = 24.0
var spacing = minSpacing
let resolvedSideInset: CGFloat = 16.0 + sideInset
var leftOffset: CGFloat = resolvedSideInset
var longTitlesWidth: CGFloat = resolvedSideInset
var titlesWidth: CGFloat = 0.0
for i in 0 ..< tabSizes.count {
let (_, paneNodeSize, _, _) = tabSizes[i]
longTitlesWidth += paneNodeSize.width
titlesWidth += paneNodeSize.width
if i != tabSizes.count - 1 {
longTitlesWidth += minSpacing
}
}
longTitlesWidth += resolvedSideInset
if longTitlesWidth < size.width && hasSelection {
spacing = (size.width - titlesWidth - resolvedSideInset * 2.0) / CGFloat(tabSizes.count - 1)
}
let verticalOffset: CGFloat = -4.0
for i in 0 ..< tabSizes.count {
let (_, paneNodeSize, paneNode, wasAdded) = tabSizes[i]
let itemNodeTransition = transition
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0) + verticalOffset), size: paneNodeSize)
var effectiveWasAdded = wasAdded
if !effectiveWasAdded && !self.bounds.intersects(self.scrollNode.convert(paneNode.frame, to: self)) && self.bounds.intersects(self.scrollNode.convert(paneFrame, to: self)) {
effectiveWasAdded = true
}
if effectiveWasAdded {
paneNode.frame = paneFrame
paneNode.alpha = 0.0
paneNode.subnodeTransform = CATransform3DMakeScale(0.1, 0.1, 1.0)
itemNodeTransition.updateSublayerTransformScale(node: paneNode, scale: 1.0)
itemNodeTransition.updateAlpha(node: paneNode, alpha: 1.0)
} else {
if self.bounds.intersects(self.scrollNode.convert(paneFrame, to: self)) {
itemNodeTransition.updateFrameAdditive(node: paneNode, frame: paneFrame)
} else if paneNode.frame != paneFrame {
paneNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) { [weak paneNode] _ in
paneNode?.frame = paneFrame
}
}
}
paneNode.updateArea(size: paneFrame.size, sideInset: spacing / 2.0, transition: itemNodeTransition)
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -spacing / 2.0, bottom: 0.0, right: -spacing / 2.0)
selectionFrames.append(paneFrame)
leftOffset += paneNodeSize.width + spacing
}
leftOffset -= spacing
leftOffset += resolvedSideInset
self.scrollNode.view.contentSize = CGSize(width: leftOffset, height: size.height)
var selectedFrame: CGRect?
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)))
}
if currentIndex != 0 && transitionFraction > 0.0 {
let currentFrame = selectionFrames[currentIndex]
let previousFrame = selectionFrames[currentIndex - 1]
selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction))
} else if currentIndex != filters.count - 1 && transitionFraction < 0.0 {
let currentFrame = selectionFrames[currentIndex]
let previousFrame = selectionFrames[currentIndex + 1]
selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction))
} else {
selectedFrame = selectionFrames[currentIndex]
}
}
if let selectedFrame = selectedFrame {
let wasAdded = self.selectedLineNode.alpha < 1.0
let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 3.0), size: CGSize(width: selectedFrame.width, height: 3.0))
if wasAdded {
self.selectedLineNode.frame = lineFrame
self.selectedLineNode.alpha = 0.0
} else {
transition.updateFrame(node: self.selectedLineNode, frame: lineFrame)
}
transition.updateAlpha(node: self.selectedLineNode, alpha: 1.0)
if let previousSelectedFrame = self.previousSelectedFrame {
let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0)))
if abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 {
focusOnSelectedFilter = true
}
}
if focusOnSelectedFilter {
let updatedBounds: CGRect
if transitionFraction.isZero && selectedFilter == filters.first?.id {
updatedBounds = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
} else if transitionFraction.isZero && selectedFilter == filters.last?.id {
updatedBounds = CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size)
} else {
let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0)))
updatedBounds = CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)
}
self.scrollNode.bounds = updatedBounds
}
transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX)
self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0)
self.previousSelectedFrame = selectedFrame
} else {
transition.updateAlpha(node: self.selectedLineNode, alpha: 0.0)
self.previousSelectedAbsFrame = nil
self.previousSelectedFrame = nil
}
if updated && self.scrollNode.view.contentOffset.x > 0.0 {
self.scrollNode.view.contentOffset = CGPoint()
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,192 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import AppBundle
final class ChatListSearchMessageSelectionPanelNode: ASDisplayNode {
private let context: AccountContext
private var theme: PresentationTheme
private let deleteMessages: () -> Void
private let shareMessages: () -> Void
private let forwardMessages: () -> Void
private let displayCopyProtectionTip: (UIView, Bool) -> Void
private let separatorNode: ASDisplayNode
private let backgroundNode: NavigationBackgroundNode
private let deleteButton: HighlightableButtonNode
private let forwardButton: HighlightableButtonNode
private let shareButton: HighlightableButtonNode
private var actions: ChatAvailableMessageActions?
private let canDeleteMessagesDisposable = MetaDisposable()
private var validLayout: ContainerViewLayout?
var chatAvailableMessageActions: ((Set<MessageId>) -> Signal<ChatAvailableMessageActions, NoError>)?
var selectedMessages = Set<MessageId>() {
didSet {
if oldValue != self.selectedMessages {
self.forwardButton.isEnabled = self.selectedMessages.count != 0
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
if self.selectedMessages.isEmpty {
self.actions = nil
if let layout = self.validLayout {
let _ = self.update(layout: layout, presentationData: presentationData, transition: .immediate)
}
self.canDeleteMessagesDisposable.set(nil)
} else {
if let chatAvailableMessageActions = self.chatAvailableMessageActions {
self.canDeleteMessagesDisposable.set((chatAvailableMessageActions(self.selectedMessages)
|> deliverOnMainQueue).startStrict(next: { [weak self] actions in
if let strongSelf = self {
strongSelf.actions = actions
if let layout = strongSelf.validLayout {
let _ = strongSelf.update(layout: layout, presentationData: presentationData, transition: .immediate)
}
}
}))
}
}
}
}
}
init(context: AccountContext, deleteMessages: @escaping () -> Void, shareMessages: @escaping () -> Void, forwardMessages: @escaping () -> Void, displayCopyProtectionTip: @escaping (UIView, Bool) -> Void) {
self.context = context
self.deleteMessages = deleteMessages
self.shareMessages = shareMessages
self.forwardMessages = forwardMessages
self.displayCopyProtectionTip = displayCopyProtectionTip
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.theme = presentationData.theme
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = presentationData.theme.chat.inputPanel.panelSeparatorColor
self.backgroundNode = NavigationBackgroundNode(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
self.deleteButton = HighlightableButtonNode(pointerStyle: .default)
self.deleteButton.isAccessibilityElement = true
self.deleteButton.accessibilityLabel = presentationData.strings.VoiceOver_MessageContextDelete
self.forwardButton = HighlightableButtonNode(pointerStyle: .default)
self.forwardButton.isAccessibilityElement = true
self.forwardButton.accessibilityLabel = presentationData.strings.VoiceOver_MessageContextForward
self.shareButton = HighlightableButtonNode(pointerStyle: .default)
self.shareButton.isAccessibilityElement = true
self.shareButton.accessibilityLabel = presentationData.strings.VoiceOver_MessageContextShare
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.deleteButton)
self.addSubnode(self.forwardButton)
self.addSubnode(self.shareButton)
self.addSubnode(self.separatorNode)
self.deleteButton.isEnabled = false
self.forwardButton.isImplicitlyDisabled = true
self.shareButton.isImplicitlyDisabled = true
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), forControlEvents: .touchUpInside)
self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), forControlEvents: .touchUpInside)
self.shareButton.addTarget(self, action: #selector(self.shareButtonPressed), forControlEvents: .touchUpInside)
}
deinit {
self.canDeleteMessagesDisposable.dispose()
}
func update(layout: ContainerViewLayout, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = layout
if presentationData.theme !== self.theme {
self.theme = presentationData.theme
self.backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
}
let width = layout.size.width
let insets = layout.insets(options: [])
let leftInset = insets.left + layout.safeInsets.left
let rightInset = insets.right + layout.safeInsets.right
let panelHeight: CGFloat
if case .regular = layout.metrics.widthClass, case .regular = layout.metrics.heightClass {
panelHeight = 49.0
} else {
panelHeight = 45.0
}
if let actions = self.actions {
self.deleteButton.isEnabled = false
self.forwardButton.isImplicitlyDisabled = !actions.options.contains(.forward)
self.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty
self.shareButton.isImplicitlyDisabled = actions.options.intersection([.forward]).isEmpty
} else {
self.deleteButton.isEnabled = false
self.forwardButton.isImplicitlyDisabled = true
self.shareButton.isImplicitlyDisabled = true
}
self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
let panelHeightWithInset = panelHeight + layout.intrinsicInsets.bottom
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeightWithInset)))
self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition)
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
return panelHeightWithInset
}
@objc func deleteButtonPressed() {
self.deleteMessages()
}
@objc func forwardButtonPressed() {
if let actions = self.actions, actions.isCopyProtected {
self.displayCopyProtectionTip(self.forwardButton.view, false)
} else {
self.forwardMessages()
}
}
@objc func shareButtonPressed() {
if let actions = self.actions, actions.isCopyProtected {
self.displayCopyProtectionTip(self.shareButton.view, true)
} else {
self.shareMessages()
}
}
}
@@ -0,0 +1,627 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import TelegramCore
import AccountContext
import ContextUI
import AnimationCache
import MultiAnimationRenderer
import TelegramNotices
protocol ChatListSearchPaneNode: ASDisplayNode {
var isReady: Signal<Bool, NoError> { get }
var isCurrent: Bool { get set }
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
func scrollToTop() -> Bool
func cancelPreviewGestures()
func transitionNodeForGallery(messageId: EngineMessage.Id, media: EngineMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
func addToTransitionSurface(view: UIView)
func updateHiddenMedia()
func updateSelectedMessages(animated: Bool)
func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)?
func didBecomeFocused()
func removeAds()
var searchCurrentMessages: [EngineMessage]? { get }
}
final class ChatListSearchPaneWrapper {
let key: ChatListSearchPaneKey
let node: ChatListSearchPaneNode
var isAnimatingOut: Bool = false
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, PresentationData)?
init(key: ChatListSearchPaneKey, node: ChatListSearchPaneNode) {
self.key = key
self.node = node
}
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
if let (currentSize, currentSideInset, currentBottomInset, _, currentPresentationData) = self.appliedParams {
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset && currentPresentationData === presentationData {
return
}
}
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, presentationData)
self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: synchronous, transition: transition)
}
}
public enum ChatListSearchPaneKey {
case chats
case topics
case publicPosts
case channels
case apps
case globalPosts
case media
case downloads
case links
case files
case music
case voice
case instantVideo
}
extension ChatListSearchPaneKey {
var filter: ChatListSearchFilter {
switch self {
case .chats:
return .chats
case .topics:
return .topics
case .publicPosts:
return .publicPosts
case .channels:
return .channels
case .apps:
return .apps
case .globalPosts:
return .globalPosts
case .media:
return .media
case .downloads:
return .downloads
case .links:
return .links
case .files:
return .files
case .music:
return .music
case .voice:
return .voice
case .instantVideo:
return .instantVideo
}
}
}
func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool, hasPublicPosts: Bool) -> [ChatListSearchPaneKey] {
var result: [ChatListSearchPaneKey] = []
if isForum {
result.append(.topics)
} else {
result.append(.chats)
}
if hasPublicPosts {
result.append(.publicPosts)
}
result.append(.channels)
result.append(.apps)
if !isForum {
result.append(.globalPosts)
}
result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice])
if !hasDownloads {
result.removeAll(where: { $0 == .downloads })
}
return result
}
struct ChatListSearchPaneSpecifier: Equatable {
var key: ChatListSearchPaneKey
var title: String
}
private 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)))
}
private final class ChatListSearchPendingPane {
let pane: ChatListSearchPaneWrapper
private var disposable: Disposable?
var isReady: Bool = false
init(
context: AccountContext,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
interaction: ChatListSearchInteraction,
navigationController: NavigationController?,
parentController: ViewController?,
peersFilter: ChatListNodePeersFilter,
requestPeerType: [ReplyMarkupButtonRequestPeerType]?,
location: ChatListControllerLocation,
searchQuery: Signal<String?, NoError>,
searchOptions: Signal<ChatListSearchOptions?, NoError>,
globalPeerSearchContext: GlobalPeerSearchContext?,
key: ChatListSearchPaneKey,
hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void
) {
let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController, parentController: parentController, globalPeerSearchContext: globalPeerSearchContext)
self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode)
self.disposable = (paneNode.isReady
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
self?.isReady = true
hasBecomeReady(key)
}).strict()
}
deinit {
self.disposable?.dispose()
}
}
final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegate {
private let context: AccountContext
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
private let peersFilter: ChatListNodePeersFilter
private let requestPeerType: [ReplyMarkupButtonRequestPeerType]?
var location: ChatListControllerLocation
private let searchQuery: Signal<String?, NoError>
private let searchOptions: Signal<ChatListSearchOptions?, NoError>
private let globalPeerSearchContext: GlobalPeerSearchContext
private let navigationController: NavigationController?
private weak var parentController: ViewController?
var interaction: ChatListSearchInteraction?
let isReady = Promise<Bool>()
var didSetIsReady = false
var isAdjacentLoadingEnabled = false
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, [ChatListSearchPaneKey])?
private(set) var currentPaneKey: ChatListSearchPaneKey?
var pendingSwitchToPaneKey: ChatListSearchPaneKey?
var currentPane: ChatListSearchPaneWrapper? {
if let currentPaneKey = self.currentPaneKey {
return self.currentPanes[currentPaneKey]
} else {
return nil
}
}
var currentPanes: [ChatListSearchPaneKey: ChatListSearchPaneWrapper] = [:]
private var pendingPanes: [ChatListSearchPaneKey: ChatListSearchPendingPane] = [:]
private var transitionFraction: CGFloat = 0.0
var currentPaneUpdated: ((ChatListSearchPaneKey?, CGFloat, ContainedViewLayoutTransition) -> Void)?
var requestExpandTabs: (() -> Bool)?
var requesDismissInput: (() -> Void)?
private var currentAvailablePanes: [ChatListSearchPaneKey]?
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?, parentController: ViewController?) {
self.context = context
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.updatedPresentationData = updatedPresentationData
self.peersFilter = peersFilter
self.requestPeerType = requestPeerType
self.location = location
self.searchQuery = searchQuery
self.searchOptions = searchOptions
self.navigationController = navigationController
self.parentController = parentController
self.globalPeerSearchContext = GlobalPeerSearchContext()
super.init()
}
func requestSelectPane(_ key: ChatListSearchPaneKey) {
if self.currentPaneKey == key {
if let requestExpandTabs = self.requestExpandTabs, requestExpandTabs() {
} else {
let _ = self.currentPane?.node.scrollToTop()
}
return
}
if key == .globalPosts {
let _ = ApplicationSpecificNotice.incrementGlobalPostsSearch(accountManager: self.context.sharedContext.accountManager).startStandalone()
}
#if DEBUG
#else
self.isAdjacentLoadingEnabled = true
#endif
if self.currentPanes[key] != nil {
self.currentPaneKey = key
if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams {
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.4, curve: .spring))
}
if case .apps = key {
self.requesDismissInput?()
}
} else if self.pendingSwitchToPaneKey != key {
self.pendingSwitchToPaneKey = key
if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams {
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.4, curve: .spring))
}
if case .apps = key {
self.requesDismissInput?()
}
}
}
override func didLoad() {
super.didLoad()
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
guard let strongSelf = self, let (_, _, _, _, _, availablePanes) = strongSelf.currentParams, let currentPaneKey = strongSelf.currentPaneKey, let index = availablePanes.firstIndex(of: currentPaneKey) else {
return []
}
if index == 0 {
return .left
}
return [.left, .right]
})
panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gesture in gestureRecognizers {
if let gesture = gesture as? ContextGesture {
gesture.cancel()
}
}
}
for subview in view.subviews {
cancelContextGestures(view: subview)
}
}
cancelContextGestures(view: self.view)
case .changed:
if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) {
self.isAdjacentLoadingEnabled = true
let translation = recognizer.translation(in: self.view)
var transitionFraction = translation.x / size.width
if currentIndex <= 0 {
transitionFraction = min(0.0, transitionFraction)
}
if currentIndex >= availablePanes.count - 1 {
transitionFraction = max(0.0, transitionFraction)
}
self.transitionFraction = transitionFraction
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .immediate)
}
case .cancelled, .ended:
if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) {
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
} else {
if abs(translation.x) > size.width / 2.0 {
directionIsToRight = translation.x > size.width / 2.0
}
}
if let directionIsToRight = directionIsToRight {
var updatedIndex = currentIndex
if directionIsToRight {
updatedIndex = min(updatedIndex + 1, availablePanes.count - 1)
} else {
updatedIndex = max(updatedIndex - 1, 0)
}
let switchToKey = availablePanes[updatedIndex]
if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{
self.currentPaneKey = switchToKey
if case .apps = switchToKey {
self.requesDismissInput?()
}
}
}
self.transitionFraction = 0.0
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.35, curve: .spring))
}
default:
break
}
}
func scrollToTop() -> Bool {
if let currentPane = self.currentPane {
return currentPane.node.scrollToTop()
} else {
return false
}
}
func updateHiddenMedia() {
self.currentPane?.node.updateHiddenMedia()
}
func transitionNodeForGallery(messageId: EngineMessage.Id, media: EngineMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return self.currentPane?.node.transitionNodeForGallery(messageId: messageId, media: media)
}
func updateSelectedMessageIds(_ selectedMessageIds: Set<EngineMessage.Id>?, animated: Bool) {
for (_, pane) in self.currentPanes {
pane.node.updateSelectedMessages(animated: animated)
}
for (_, pane) in self.pendingPanes {
pane.pane.node.updateSelectedMessages(animated: animated)
}
}
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, availablePanes: [ChatListSearchPaneKey], transition: ContainedViewLayoutTransition) {
let previousAvailablePanes = self.currentAvailablePanes ?? []
self.currentAvailablePanes = availablePanes
if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) {
var nextCandidatePaneKey: ChatListSearchPaneKey?
if let index = previousAvailablePanes.firstIndex(of: currentPaneKey), index != 0 {
for i in (0 ... index - 1).reversed() {
if availablePanes.contains(previousAvailablePanes[i]) {
nextCandidatePaneKey = previousAvailablePanes[i]
}
}
}
if nextCandidatePaneKey == nil {
nextCandidatePaneKey = availablePanes.first
}
if let nextCandidatePaneKey = nextCandidatePaneKey {
self.pendingSwitchToPaneKey = nextCandidatePaneKey
} else {
self.currentPaneKey = nil
self.pendingSwitchToPaneKey = nil
}
} else if self.currentPaneKey == nil && self.pendingSwitchToPaneKey == nil {
self.pendingSwitchToPaneKey = availablePanes.first
}
let currentIndex: Int?
if let currentPaneKey = self.currentPaneKey {
currentIndex = availablePanes.firstIndex(of: currentPaneKey)
} else {
currentIndex = nil
}
self.currentParams = (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes)
switch self.location {
case .forum, .savedMessagesChats:
self.backgroundColor = .clear
default:
self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
}
let paneFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))
var visiblePaneIndices: [Int] = []
var requiredPendingKeys: [ChatListSearchPaneKey] = []
if let currentIndex = currentIndex {
if currentIndex != 0 && self.isAdjacentLoadingEnabled {
visiblePaneIndices.append(currentIndex - 1)
}
visiblePaneIndices.append(currentIndex)
if currentIndex != availablePanes.count - 1 && self.isAdjacentLoadingEnabled {
visiblePaneIndices.append(currentIndex + 1)
}
for index in visiblePaneIndices {
let key = availablePanes[index]
if self.currentPanes[key] == nil && self.pendingPanes[key] == nil {
requiredPendingKeys.append(key)
}
}
}
if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey {
if self.currentPanes[pendingSwitchToPaneKey] == nil && self.pendingPanes[pendingSwitchToPaneKey] == nil {
if !requiredPendingKeys.contains(pendingSwitchToPaneKey) {
requiredPendingKeys.append(pendingSwitchToPaneKey)
}
}
}
for key in requiredPendingKeys {
if self.pendingPanes[key] == nil {
var leftScope = false
let pane = ChatListSearchPendingPane(
context: self.context,
animationCache: self.animationCache,
animationRenderer: self.animationRenderer,
updatedPresentationData: self.updatedPresentationData,
interaction: self.interaction!,
navigationController: self.navigationController,
parentController: self.parentController,
peersFilter: self.peersFilter,
requestPeerType: self.requestPeerType,
location: self.location,
searchQuery: self.searchQuery,
searchOptions: self.searchOptions,
globalPeerSearchContext: self.globalPeerSearchContext,
key: key,
hasBecomeReady: { [weak self] key in
let apply: () -> Void = {
guard let strongSelf = self else {
return
}
if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = strongSelf.currentParams {
var transition: ContainedViewLayoutTransition = .immediate
if strongSelf.pendingSwitchToPaneKey == key && strongSelf.currentPaneKey != nil {
transition = .animated(duration: 0.4, curve: .spring)
}
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: transition)
}
}
if leftScope {
apply()
}
}
)
self.pendingPanes[key] = pane
pane.pane.node.frame = paneFrame
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: .immediate)
leftScope = true
}
}
for (key, pane) in self.pendingPanes {
pane.pane.node.frame = paneFrame
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate)
if pane.isReady {
self.pendingPanes.removeValue(forKey: key)
self.currentPanes[key] = pane.pane
}
}
var paneDefaultTransition = transition
var previousPaneKey: ChatListSearchPaneKey?
var paneSwitchAnimationOffset: CGFloat = 0.0
var updatedCurrentIndex = currentIndex
if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, let _ = self.currentPanes[pendingSwitchToPaneKey] {
self.pendingSwitchToPaneKey = nil
previousPaneKey = self.currentPaneKey
self.currentPaneKey = pendingSwitchToPaneKey
updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey)
if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex {
if updatedCurrentIndex < previousIndex {
paneSwitchAnimationOffset = -size.width
} else {
paneSwitchAnimationOffset = size.width
}
}
paneDefaultTransition = .immediate
}
for (key, pane) in self.currentPanes {
if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex {
var paneWasAdded = false
if pane.node.supernode == nil {
self.addSubnode(pane.node)
paneWasAdded = true
}
let indexOffset = CGFloat(index - updatedCurrentIndex)
let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : paneDefaultTransition
let adjustedFrame = paneFrame.offsetBy(dx: size.width * self.transitionFraction + indexOffset * size.width, dy: 0.0)
let paneCompletion: () -> Void = { [weak self, weak pane] in
guard let strongSelf = self, let pane = pane else {
return
}
pane.isAnimatingOut = false
if let _ = strongSelf.currentParams {
if let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey), let paneIndex = availablePanes.firstIndex(of: key), paneIndex == 0 || abs(paneIndex - currentIndex) <= 1 {
} else {
if let pane = strongSelf.currentPanes.removeValue(forKey: key) {
pane.node.removeFromSupernode()
}
}
}
}
if let previousPaneKey = previousPaneKey, key == previousPaneKey {
pane.node.frame = adjustedFrame
let isAnimatingOut = pane.isAnimatingOut
pane.isAnimatingOut = true
transition.animateFrame(node: pane.node, from: paneFrame, to: paneFrame.offsetBy(dx: -paneSwitchAnimationOffset, dy: 0.0), completion: isAnimatingOut ? nil : { _ in
paneCompletion()
})
} else if let _ = previousPaneKey, key == self.currentPaneKey {
pane.node.frame = adjustedFrame
let isAnimatingOut = pane.isAnimatingOut
pane.isAnimatingOut = true
transition.animatePositionAdditive(node: pane.node, offset: CGPoint(x: paneSwitchAnimationOffset, y: 0.0), completion: isAnimatingOut ? nil : {
paneCompletion()
})
} else {
let isAnimatingOut = pane.isAnimatingOut
pane.isAnimatingOut = true
paneTransition.updateFrame(node: pane.node, frame: adjustedFrame, completion: isAnimatingOut ? nil : { _ in
paneCompletion()
})
}
pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
pane.node.isCurrent = key == self.currentPaneKey
if paneWasAdded && key == self.currentPaneKey {
pane.node.didBecomeFocused()
}
}
}
for (_, pane) in self.pendingPanes {
let paneTransition: ContainedViewLayoutTransition = .immediate
paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame)
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: paneTransition)
}
if !self.didSetIsReady {
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
self.didSetIsReady = true
self.isReady.set(currentPane.node.isReady)
} else if self.pendingSwitchToPaneKey == nil {
self.didSetIsReady = true
self.isReady.set(.single(true))
}
}
self.currentPaneUpdated?(self.currentPaneKey, self.transitionFraction, transition)
}
func allCurrentMessages() -> [EngineMessage.Id: EngineMessage] {
var allMessages: [EngineMessage.Id: EngineMessage] = [:]
for (_, pane) in self.currentPanes {
if let messages = pane.node.searchCurrentMessages {
for message in messages {
allMessages[message.id] = message
}
}
}
return allMessages
}
}
@@ -0,0 +1,87 @@
import Foundation
import UIKit
import SwiftSignalKit
import TelegramCore
import AccountContext
enum ChatListSelectionReadOption: Equatable {
case all(enabled: Bool)
case selective(enabled: Bool)
}
struct ChatListSelectionOptions: Equatable {
let read: ChatListSelectionReadOption
let delete: Bool
}
func chatListSelectionOptions(context: AccountContext, peerIds: Set<EnginePeer.Id>, filterId: Int32?) -> Signal<ChatListSelectionOptions, NoError> {
if peerIds.isEmpty {
if let filterId = filterId {
return chatListFilterItems(context: context)
|> map { filterItems -> ChatListSelectionOptions in
for (filter, unreadCount, _) in filterItems.1 {
if filter.id == filterId {
return ChatListSelectionOptions(read: .all(enabled: unreadCount != 0), delete: false)
}
}
return ChatListSelectionOptions(read: .all(enabled: false), delete: false)
}
|> distinctUntilChanged
} else {
return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.TotalReadCounters())
|> map { readCounters -> ChatListSelectionOptions in
var hasUnread = false
if readCounters.count(for: .raw, in: .chats, with: .all) != 0 {
hasUnread = true
}
return ChatListSelectionOptions(read: .all(enabled: hasUnread), delete: false)
}
|> distinctUntilChanged
}
} else {
return context.engine.data.subscribe(EngineDataList(
peerIds.map(TelegramEngine.EngineData.Item.Messages.PeerReadCounters.init)
))
|> map { readCounters -> ChatListSelectionOptions in
var hasUnread = false
for counters in readCounters {
if counters.isUnread {
hasUnread = true
break
}
}
return ChatListSelectionOptions(read: .selective(enabled: hasUnread), delete: true)
}
|> distinctUntilChanged
}
}
func forumSelectionOptions(context: AccountContext, peerId: EnginePeer.Id, threadIds: Set<Int64>) -> Signal<ChatListSelectionOptions, NoError> {
return context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
EngineDataList(threadIds.map { TelegramEngine.EngineData.Item.Peer.ThreadData(id: peerId, threadId: $0) })
)
|> map { peer, threadDatas -> ChatListSelectionOptions in
guard !threadIds.isEmpty, case let .channel(channel) = peer else {
return ChatListSelectionOptions(read: .selective(enabled: false), delete: false)
}
var canDelete = !threadIds.contains(1)
if !channel.hasPermission(.deleteAllMessages) {
canDelete = false
}
var hasUnread = false
for thread in threadDatas {
guard let thread = thread else {
continue
}
if thread.incomingUnreadCount > 0 {
hasUnread = true
break
}
}
return ChatListSelectionOptions(read: .selective(enabled: hasUnread), delete: canDelete)
}
}
@@ -0,0 +1,301 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AccountContext
import AnimationCache
import MultiAnimationRenderer
import TelegramCore
private final class ShimmerEffectNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode
private let imageNode: ASImageNode
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
override init() {
self.imageNodeContainer = ASDisplayNode()
self.imageNodeContainer.isLayerBacked = true
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.contentMode = .scaleToFill
super.init()
self.isLayerBacked = true
self.clipsToBounds = true
self.imageNodeContainer.addSubnode(self.imageNode)
self.addSubnode(self.imageNodeContainer)
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
self.isCurrentlyInHierarchy = true
self.updateAnimation()
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.isCurrentlyInHierarchy = false
self.updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
}
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
if sizeUpdated {
if self.shouldBeAnimating {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
}
}
if frameUpdated {
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
self.updateAnimation()
}
private func updateAnimation() {
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
if shouldBeAnimating != self.shouldBeAnimating {
self.shouldBeAnimating = shouldBeAnimating
if shouldBeAnimating {
self.addImageAnimation()
} else {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else {
return
}
let gradientHeight: CGFloat = 250.0
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
animation.beginTime = 1.0
self.imageNode.layer.add(animation, forKey: "shimmer")
}
}
public final class ChatListShimmerNode: ASDisplayNode {
private let backgroundColorNode: ASDisplayNode
private let effectNode: ShimmerEffectNode
private let maskNode: ASImageNode
private var currentParams: (size: CGSize, presentationData: PresentationData)?
override public init() {
self.backgroundColorNode = ASDisplayNode()
self.effectNode = ShimmerEffectNode()
self.maskNode = ASImageNode()
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.backgroundColorNode)
self.addSubnode(self.effectNode)
self.addSubnode(self.maskNode)
}
public func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData {
self.currentParams = (size, presentationData)
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let timestamp1: Int32 = 100000
let peers: [EnginePeer.Id: EnginePeer] = [:]
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
}, openAdInfo: { _, _ in
}, openAccountFreezeInfo: {
}, openUrl: { _ in
})
interaction.isInlineMode = isInlineMode
let items = (0 ..< 2).map { _ -> ChatListItem in
let message = EngineMessage(
stableId: 0,
stableVersion: 0,
id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: timestamp1,
flags: [],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: peer1,
text: "Text",
attributes: [],
media: [],
peers: peers,
associatedMessages: [:],
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
let readState = EnginePeerReadCounters()
return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData(
messages: [message],
peer: EngineRenderedPeer(peer: peer1),
threadInfo: nil,
combinedReadState: readState,
isRemovedFromTotalUnreadCount: false,
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
draftState: nil,
mediaDraftContentType: nil,
inputActivities: nil,
promoInfo: nil,
ignoreUnreadBadge: false,
displayAsMessage: false,
hasFailedMessages: false,
forumTopicData: nil,
topForumTopicItems: [],
autoremoveTimeout: nil,
storyState: nil,
requiresPremiumForMessaging: false,
displayAsTopicList: false,
tags: []
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enabledContextActions: nil, hiddenOffset: false, interaction: interaction)
}
var itemNodes: [ChatListItemNode] = []
for i in 0 ..< items.count {
items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in
if let itemNode = node as? ChatListItemNode {
itemNodes.append(itemNode)
}
apply().1(ListViewItemApply(isOnScreen: true))
})
}
self.backgroundColorNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor
self.maskNode.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(presentationData.theme.chatList.backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
var currentY: CGFloat = 0.0
let fakeLabelPlaceholderHeight: CGFloat = 8.0
func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) {
let startPoint = origin
let diameter = fakeLabelPlaceholderHeight
context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter)))
context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter)))
}
while currentY < size.height {
let sampleIndex = 0
let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
if !isInlineMode {
if !itemNodes[sampleIndex].avatarNode.isHidden {
context.fillEllipse(in: itemNodes[sampleIndex].avatarNode.view.convert(itemNodes[sampleIndex].avatarNode.bounds, to: itemNodes[sampleIndex].view).offsetBy(dx: 0.0, dy: currentY))
}
}
let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY)
if isInlineMode {
fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 22.0, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0 - 22.0)
} else {
fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0)
}
let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY)
if isInlineMode {
context.fillEllipse(in: CGRect(origin: CGPoint(x: textFrame.minX, y: titleFrame.minY + 2.0), size: CGSize(width: 16.0, height: 16.0)))
}
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0)
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0)
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0)
let dateFrame = itemNodes[sampleIndex].dateNode.frame.offsetBy(dx: 0.0, dy: currentY)
fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0)
context.setBlendMode(.normal)
context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor)
context.fill(itemNodes[sampleIndex].separatorNode.frame.offsetBy(dx: 0.0, dy: currentY))
currentY += itemHeight
}
})
self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
}
transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
}
}
@@ -0,0 +1,178 @@
import Foundation
import TelegramPresentationData
import TelegramStringFormatting
private let telegramReleaseDate = Date(timeIntervalSince1970: 1376438400.0)
func suggestDates(for string: String, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> [(minDate: Date?, maxDate: Date, string: String?)] {
let string = string.folding(options: .diacriticInsensitive, locale: .current).trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if string.count < 3 {
return []
}
let months: [Int: (String, String)] = [
1: (strings.Month_GenJanuary, strings.Month_ShortJanuary),
2: (strings.Month_GenFebruary, strings.Month_ShortFebruary),
3: (strings.Month_GenMarch, strings.Month_ShortMarch),
4: (strings.Month_GenApril, strings.Month_ShortApril),
5: (strings.Month_GenMay, strings.Month_ShortMay),
6: (strings.Month_GenJune, strings.Month_ShortJune),
7: (strings.Month_GenJuly, strings.Month_ShortJuly),
8: (strings.Month_GenAugust, strings.Month_ShortAugust),
9: (strings.Month_GenSeptember, strings.Month_ShortSeptember),
10: (strings.Month_GenOctober, strings.Month_ShortOctober),
11: (strings.Month_GenNovember, strings.Month_ShortNovember),
12: (strings.Month_GenDecember, strings.Month_ShortDecember)
]
let weekDays: [Int: (String, String)] = [
1: (strings.Weekday_Monday, strings.Weekday_ShortMonday),
2: (strings.Weekday_Tuesday, strings.Weekday_ShortTuesday),
3: (strings.Weekday_Wednesday, strings.Weekday_ShortWednesday),
4: (strings.Weekday_Thursday, strings.Weekday_ShortThursday),
5: (strings.Weekday_Friday, strings.Weekday_ShortFriday),
6: (strings.Weekday_Saturday, strings.Weekday_ShortSaturday),
7: (strings.Weekday_Sunday, strings.Weekday_ShortSunday.lowercased()),
]
let today = strings.Weekday_Today
let yesterday = strings.Weekday_Yesterday
let dateSeparator = dateTimeFormat.dateSeparator
var result: [(Date?, Date, String?)] = []
let calendar = Calendar.current
func getLowerDate(for date: Date) -> Date {
let components = calendar.dateComponents(in: .current, from: date)
let upperComponents = DateComponents(year: components.year, month: components.month, day: components.day, hour: 0, minute: 0, second: 0)
return calendar.date(from: upperComponents)!
}
func getUpperDate(for date: Date) -> Date {
let components = calendar.dateComponents(in: .current, from: date)
let upperComponents = DateComponents(year: components.year, month: components.month, day: components.day, hour: 23, minute: 59, second: 59)
return calendar.date(from: upperComponents)!
}
let now = Date()
let nowComponents = calendar.dateComponents(in: .current, from: now)
guard let year = nowComponents.year else {
return []
}
let midnightDate = calendar.startOfDay(for: now)
if today.lowercased().hasPrefix(string) {
let todayDate = getUpperDate(for: midnightDate)
result.append((midnightDate, todayDate, today))
}
if yesterday.lowercased().hasPrefix(string) {
let yesterdayMidnight = calendar.date(byAdding: .day, value: -1, to: midnightDate)!
let yesterdayDate = getUpperDate(for: yesterdayMidnight)
result.append((yesterdayMidnight, yesterdayDate, yesterday))
}
func getLowerMonthDate(month: Int, year: Int) -> Date {
let upperComponents = DateComponents(year: year, month: month, day: 1, hour: 0, minute: 0, second: 0)
return calendar.date(from: upperComponents)!
}
func getUpperMonthDate(month: Int, year: Int) -> Date {
let monthComponents = DateComponents(year: year, month: month)
let date = calendar.date(from: monthComponents)!
let range = calendar.range(of: .day, in: .month, for: date)!
let numDays = range.count
let upperComponents = DateComponents(year: year, month: month, day: numDays, hour: 23, minute: 59, second: 59)
return calendar.date(from: upperComponents)!
}
let decimalRange = string.rangeOfCharacter(from: .decimalDigits)
if decimalRange != nil {
if string.count == 4, let value = Int(string), value <= year {
let minDate = getLowerMonthDate(month: 1, year: value)
let maxDate = getUpperMonthDate(month: 12, year: value)
if maxDate > telegramReleaseDate {
result.append((minDate, maxDate, "\(value)"))
}
} else {
do {
func process(_ date: Date) {
var resultDate = date
if resultDate > now && !calendar.isDate(resultDate, equalTo: now, toGranularity: .year) {
if let date = calendar.date(byAdding: .year, value: -1, to: resultDate) {
resultDate = date
}
}
let stringComponents = string.components(separatedBy: dateSeparator)
if stringComponents.count < 3 {
for i in 0..<8 {
if let date = calendar.date(byAdding: .year, value: -i, to: resultDate), date < now, date > telegramReleaseDate {
let lowerDate = getLowerDate(for: resultDate)
result.append((lowerDate, date, nil))
}
}
} else if resultDate < now, date > telegramReleaseDate {
let lowerDate = getLowerDate(for: resultDate)
result.append((lowerDate, resultDate, nil))
}
}
let dd = try NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue)
if let match = dd.firstMatch(in: string, options: [], range: NSMakeRange(0, string.utf16.count)), let date = match.date, date > telegramReleaseDate {
process(date)
} else if let match = dd.firstMatch(in: string.replacingOccurrences(of: ".", with: "/"), options: [], range: NSMakeRange(0, string.utf16.count)), let date = match.date, date > telegramReleaseDate {
process(date)
}
} catch {
}
}
}
for (day, value) in weekDays {
let dayName = value.0.lowercased()
let shortDayName = value.1.lowercased()
if string == shortDayName || (string.count >= shortDayName.count && dayName.hasPrefix(string)) {
var nextDateComponent = calendar.dateComponents([.hour, .minute, .second], from: now)
nextDateComponent.weekday = day + calendar.firstWeekday
if let date = calendar.nextDate(after: now, matching: nextDateComponent, matchingPolicy: .nextTime, direction: .backward) {
let lowerAnchorDate = getLowerDate(for: date)
let upperAnchorDate = getUpperDate(for: date)
for i in 0..<5 {
if let lowerDate = calendar.date(byAdding: .hour, value: -24 * 7 * i, to: lowerAnchorDate), let upperDate = calendar.date(byAdding: .hour, value: -24 * 7 * i, to: upperAnchorDate) {
if calendar.isDate(upperDate, equalTo: now, toGranularity: .weekOfYear) {
result.append((lowerDate, upperDate, value.0))
} else {
result.append((lowerDate, upperDate, nil))
}
}
}
}
}
}
let cleanString = string.trimmingCharacters(in: .decimalDigits).trimmingCharacters(in: .whitespacesAndNewlines)
let cleanDigits = string.trimmingCharacters(in: .letters).trimmingCharacters(in: .whitespacesAndNewlines)
for (month, value) in months {
let monthName = value.0.lowercased()
let shortMonthName = value.1.lowercased()
if cleanString == shortMonthName || (cleanString.count >= shortMonthName.count && monthName.hasPrefix(cleanString)) {
if cleanDigits.count == 4, let year = Int(cleanDigits) {
let lowerDate = getLowerMonthDate(month: month, year: year)
let upperDate = getUpperMonthDate(month: month, year: year)
if upperDate <= now && upperDate > telegramReleaseDate {
result.append((lowerDate, upperDate, stringForMonth(strings: strings, month: Int32(month - 1), ofYear: Int32(year - 1900))))
}
} else if cleanDigits.isEmpty {
for i in (year - 7 ... year).reversed() {
let lowerDate = getUpperMonthDate(month: month, year: i)
let upperDate = getUpperMonthDate(month: month, year: i)
if upperDate <= now && upperDate > telegramReleaseDate {
result.append((lowerDate, upperDate, stringForMonth(strings: strings, month: Int32(month - 1), ofYear: Int32(i - 1900))))
}
}
}
}
}
return result
}
@@ -0,0 +1,283 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TextNodeWithEntities
import AccountContext
import ItemListUI
import ComponentFlow
import ListComposePollOptionComponent
import TextFieldComponent
public class ItemListFilterTitleInputItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let text: NSAttributedString
let enableAnimations: Bool
let placeholder: String
let maxLength: Int
let inputMode: ListComposePollOptionComponent.InputMode?
let enabled: Bool
public let sectionId: ItemListSectionId
let textUpdated: (NSAttributedString) -> Void
let updatedFocus: ((Bool) -> Void)?
let toggleInputMode: () -> Void
public let tag: ItemListItemTag?
public init(
context: AccountContext,
presentationData: ItemListPresentationData,
systemStyle: ItemListSystemStyle,
text: NSAttributedString,
enableAnimations: Bool,
placeholder: String,
maxLength: Int = 0,
inputMode: ListComposePollOptionComponent.InputMode?,
enabled: Bool = true,
tag: ItemListItemTag? = nil,
sectionId: ItemListSectionId,
textUpdated: @escaping (NSAttributedString) -> Void,
updatedFocus: ((Bool) -> Void)? = nil,
toggleInputMode: @escaping () -> Void
) {
self.context = context
self.presentationData = presentationData
self.systemStyle = systemStyle
self.text = text
self.enableAnimations = enableAnimations
self.placeholder = placeholder
self.maxLength = maxLength
self.inputMode = inputMode
self.enabled = enabled
self.tag = tag
self.sectionId = sectionId
self.textUpdated = textUpdated
self.updatedFocus = updatedFocus
self.toggleInputMode = toggleInputMode
}
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 = ItemListFilterTitleInputItemNode()
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? ItemListFilterTitleInputItemNode {
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 class ItemListFilterTitleInputItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemNode, ItemListItemFocusableNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
let textFieldState = TextFieldComponent.ExternalState()
private let textField = ComponentView<Empty>()
private let componentState = EmptyComponentState()
private var item: ItemListFilterTitleInputItem?
public var tag: ItemListItemTag? {
return self.item?.tag
}
var textFieldView: ListComposePollOptionComponent.View? {
return self.textField.view as? ListComposePollOptionComponent.View
}
public 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()
super.init(layerBacked: false, dynamicBounce: false)
}
override public func didLoad() {
super.didLoad()
}
public func asyncLayout() -> (_ item: ItemListFilterTitleInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { [weak self] item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let leftInset: CGFloat = 16.0 + params.leftInset
let rightInset: CGFloat = 16.0 + params.rightInset
let _ = rightInset
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let contentSize = CGSize(width: params.width, height: item.systemStyle == .glass ? 52.0 : 44.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor)
let _ = attributedPlaceholderText
return (layout, {
guard let self else {
return
}
self.item = item
if let _ = updatedTheme {
self.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
self.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
self.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
}
if self.backgroundNode.supernode == nil {
self.insertSubnode(self.backgroundNode, at: 0)
}
if self.topStripeNode.supernode == nil {
self.insertSubnode(self.topStripeNode, at: 1)
}
if self.bottomStripeNode.supernode == nil {
self.insertSubnode(self.bottomStripeNode, at: 2)
}
if self.maskNode.supernode == nil {
self.insertSubnode(self.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
self.topStripeNode.isHidden = true
default:
hasTopCorners = true
self.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
self.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
self.bottomStripeNode.isHidden = hasCorners
}
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
self.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)))
self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
self.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
self.textField.parentState = self.componentState
self.componentState._updated = { [weak self] transition, _ in
guard let self, let item = self.item else {
return
}
guard let textFieldView = self.textFieldView else {
return
}
item.textUpdated(textFieldView.currentAttributedText)
}
let textFieldSize = self.textField.update(
transition: .immediate,
component: AnyComponent(ListComposePollOptionComponent(
externalState: self.textFieldState,
context: item.context,
theme: item.presentationData.theme,
strings: item.presentationData.strings,
placeholder: NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPlaceholderTextColor),
resetText: self.textField.view == nil ? ListComposePollOptionComponent.ResetText(value: item.text) : nil,
characterLimit: item.maxLength,
enableInlineAnimations: item.enableAnimations,
emptyLineHandling: .notAllowed,
returnKeyAction: { [weak self] in
guard let self else {
return
}
let _ = self
},
backspaceKeyAction: nil,
selection: nil,
inputMode: item.inputMode,
alwaysDisplayInputModeSelector: true,
toggleInputMode: { [weak self] in
guard let self else {
return
}
self.item?.toggleInputMode()
}
)),
environment: {},
containerSize: CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: layout.size.height)
)
let textFieldFrame = CGRect(origin: CGPoint(x: params.leftInset, y: floorToScreenPixels((layoutSize.height - textFieldSize.height) / 2.0)), size: textFieldSize)
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.view.addSubview(textFieldView)
}
textFieldView.frame = textFieldFrame
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
public func focus() {
if let textFieldView = self.textField.view as? ListComposePollOptionComponent.View {
textFieldView.activateInput()
}
}
public func selectAll() {
}
}
@@ -0,0 +1,272 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import ListSectionHeaderNode
import AppBundle
class ChatListArchiveInfoItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let selectable: Bool = false
init(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
}
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 = ChatListArchiveInfoItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply()
})
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ChatListArchiveInfoItemNode)
if let nodeValue = node() as? ChatListArchiveInfoItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem == nil)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
private let separatorHeight = 1.0 / UIScreen.main.scale
private let titleFont = Font.regular(20.0)
private let textFont = Font.regular(15.0)
private final class InfoPageNode: ASDisplayNode {
private let iconNodeBase: ASImageNode
private let iconNodeContent: ASImageNode
private let titleNode: TextNode
private let textNode: TextNode
private var theme: PresentationTheme?
override init() {
self.iconNodeBase = ASImageNode()
self.iconNodeBase.displaysAsynchronously = false
self.iconNodeBase.displayWithoutProcessing = true
self.iconNodeContent = ASImageNode()
self.iconNodeContent.displaysAsynchronously = false
self.iconNodeContent.displayWithoutProcessing = true
self.titleNode = TextNode()
self.titleNode.displaysAsynchronously = false
self.textNode = TextNode()
self.textNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.iconNodeBase)
self.addSubnode(self.iconNodeContent)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
}
func asyncLayout() -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ width: CGFloat, _ index: Int) -> (CGFloat, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
return { [weak self] theme, strings, width, index in
let title: String
let text: String
if index == 0 {
title = strings.ArchivedChats_IntroTitle1
text = strings.ArchivedChats_IntroText1
} else if index == 1 {
title = strings.ArchivedChats_IntroTitle2
text = strings.ArchivedChats_IntroText2
} else {
title = strings.ArchivedChats_IntroTitle3
text = strings.ArchivedChats_IntroText3
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: nil), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: min(300.0, width - 16.0), height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: textFont, textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: nil), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: min(300.0, width - 16.0), height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let topContentInset: CGFloat = 98.0
let bottomContentInset: CGFloat = 64.0 + 28.0
let textSpacing: CGFloat = 6.0
let contentHeight = topContentInset + titleLayout.size.height + textSpacing + textLayout.size.height + bottomContentInset
return (contentHeight, {
guard let strongSelf = self else {
return
}
if strongSelf.theme !== theme {
strongSelf.theme = theme
if index == 0 {
strongSelf.iconNodeBase.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Archive/Intro1Base"), color: theme.list.itemPrimaryTextColor)
} else {
strongSelf.iconNodeBase.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Archive/Intro2Base"), color: theme.list.itemPrimaryTextColor)
}
if index == 0 {
strongSelf.iconNodeContent.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Archive/Intro1Content"), color: theme.chatList.unreadBadgeActiveBackgroundColor)
} else if index == 1 {
strongSelf.iconNodeContent.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Archive/Intro2Content"), color: theme.chatList.unreadBadgeInactiveBackgroundColor)
} else {
strongSelf.iconNodeContent.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Archive/Intro3Content"), color: theme.chatList.unreadBadgeActiveBackgroundColor)
}
}
let topIconInset: CGFloat = 110.0
if let baseImage = strongSelf.iconNodeBase.image, let contentImage = strongSelf.iconNodeContent.image {
strongSelf.iconNodeBase.frame = CGRect(origin: CGPoint(x: floor((width - baseImage.size.width) / 2.0), y: floor((topIconInset - baseImage.size.height) / 2.0)), size: baseImage.size)
strongSelf.iconNodeContent.frame = CGRect(origin: CGPoint(x: floor((width - contentImage.size.width) / 2.0), y: floor((topIconInset - contentImage.size.height) / 2.0)), size: contentImage.size)
}
let _ = titleApply()
let _ = textApply()
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleLayout.size.width) / 2.0), y: topContentInset), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((width - textLayout.size.width) / 2.0), y: titleFrame.maxY + textSpacing), size: textLayout.size)
})
}
}
}
class ChatListArchiveInfoItemNode: ListViewItemNode, ASScrollViewDelegate {
private var item: ChatListArchiveInfoItem?
private let scrollNode: ASScrollNode
private let pageControlNode: PageControlNode
private var headerNode: ListSectionHeaderNode?
private let infoPageNodes: [InfoPageNode]
required init() {
self.scrollNode = ASScrollNode()
self.pageControlNode = PageControlNode(dotSize: 7.0, dotSpacing: 9.0, dotColor: .blue, inactiveDotColor: .gray)
self.infoPageNodes = (0 ..< 3).map({ _ in InfoPageNode() })
self.pageControlNode.pagesCount = self.infoPageNodes.count
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.scrollNode)
self.infoPageNodes.forEach(self.scrollNode.addSubnode)
self.addSubnode(self.pageControlNode)
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.isPagingEnabled = true
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.pageControlNode.setPage(0.0)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListArchiveInfoItem, params, nextItem == nil)
apply()
}
func asyncLayout() -> (_ item: ChatListArchiveInfoItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let previousItem = self.item
let makeInfoPageLayouts = self.infoPageNodes.map({ $0.asyncLayout() })
return { item, params, last in
let baseWidth = params.width - params.leftInset - params.rightInset
let bottomInset: CGFloat = 22.0 + 28.0
let themeUpdated = previousItem?.theme !== item.theme
var infoPageLayoutsAndApply: [(CGFloat, () -> Void)] = []
var maxHeight: CGFloat = 0.0
for i in 0 ..< makeInfoPageLayouts.count {
let sizeAndApply = makeInfoPageLayouts[i](item.theme, item.strings, baseWidth, i)
maxHeight = max(maxHeight, sizeAndApply.0)
infoPageLayoutsAndApply.append(sizeAndApply)
}
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: maxHeight), insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if themeUpdated {
strongSelf.pageControlNode.dotColor = item.theme.chatList.unreadBadgeActiveBackgroundColor
strongSelf.pageControlNode.inactiveDotColor = item.theme.list.pageIndicatorInactiveColor
}
let resetOffset = !strongSelf.scrollNode.frame.width.isEqual(to: baseWidth)
strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: baseWidth, height: layout.contentSize.height))
strongSelf.scrollNode.view.contentSize = CGSize(width: baseWidth * CGFloat(infoPageLayoutsAndApply.count), height: layout.contentSize.height)
if resetOffset {
strongSelf.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 0.0)
}
for i in 0 ..< infoPageLayoutsAndApply.count {
strongSelf.infoPageNodes[i].frame = CGRect(origin: CGPoint(x: baseWidth * CGFloat(i), y: 0.0), size: CGSize(width: baseWidth, height: layout.contentSize.height))
infoPageLayoutsAndApply[i].1()
}
let pageControlSize = strongSelf.pageControlNode.measure(CGSize(width: baseWidth, height: 100.0))
strongSelf.pageControlNode.frame = CGRect(origin: CGPoint(x: floor((params.width - pageControlSize.width) / 2.0), y: layout.contentSize.height - bottomInset - pageControlSize.height), size: pageControlSize)
if strongSelf.headerNode == nil {
let headerNode = ListSectionHeaderNode(theme: item.theme)
headerNode.title = item.strings.ChatList_ArchivedChatsTitle.uppercased()
strongSelf.addSubnode(headerNode)
strongSelf.headerNode = headerNode
}
if let headerNode = strongSelf.headerNode {
if themeUpdated {
headerNode.updateTheme(theme: item.theme)
}
headerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.contentSize.height - 28.0), size: CGSize(width: params.width, height: 28.0))
headerNode.updateLayout(size: CGSize(width: params.width, height: 28.0), leftInset: params.leftInset, rightInset: params.rightInset)
}
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
}
})
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let bounds = scrollView.bounds
if !bounds.width.isZero {
self.pageControlNode.setPage(scrollView.contentOffset.x / bounds.width)
}
}
}
@@ -0,0 +1,213 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
enum ChatListBadgeContent: Equatable {
case none
case blank
case text(NSAttributedString)
case mention
var text: String? {
if case let .text(text) = self {
return text.string
}
return nil
}
var isEmpty: Bool {
if case .none = self {
return true
}
return false
}
}
private func measureString(_ string: String) -> String {
let wideChar = "8"
if string.count < 2 {
return wideChar
} else {
return string[string.startIndex ..< string.index(string.endIndex, offsetBy: -1)] + wideChar
}
}
final class ChatListBadgeNode: ASDisplayNode {
let backgroundNode: ASImageNode
let textNode: TextNode
private let measureTextNode: TextNode
private var text: String?
private var content: ChatListBadgeContent?
private var isHiddenInternal = false
var disableBounce: Bool = false
override init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.measureTextNode = TextNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
}
func asyncLayout() -> (CGSize, CGFloat, UIFont, UIImage?, ChatListBadgeContent) -> (CGSize, (Bool, Bool) -> Void) {
let textLayout = TextNode.asyncLayout(self.textNode)
let measureTextLayout = TextNode.asyncLayout(self.measureTextNode)
let currentContent = self.content
return { [weak self] boundingSize, imageWidth, badgeFont, backgroundImage, content in
var badgeWidth: CGFloat = 0.0
var textLayoutAndApply: (TextNodeLayout, () -> TextNode)?
switch content {
case let .text(text):
textLayoutAndApply = textLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: boundingSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (measureLayout, _) = measureTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: measureString(text.string), font: badgeFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: boundingSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
badgeWidth = max(imageWidth, measureLayout.size.width + imageWidth / 2.0)
case .mention, .blank:
badgeWidth = imageWidth
case .none:
badgeWidth = 0.0
}
return (CGSize(width: badgeWidth, height: imageWidth), { animated, bounce in
if let strongSelf = self {
strongSelf.content = content
if let backgroundImage = backgroundImage {
strongSelf.backgroundNode.image = backgroundImage
}
if content == currentContent {
return
}
let badgeWidth = max(imageWidth, badgeWidth)
let previousBadgeWidth = !strongSelf.backgroundNode.bounds.width.isZero ? strongSelf.backgroundNode.bounds.width : badgeWidth
var animateTextNode = false
if animated {
strongSelf.isHidden = false
let currentIsEmpty = currentContent?.isEmpty ?? true
let nextIsEmpty = content.isEmpty
if !nextIsEmpty {
if case .text = content {
strongSelf.textNode.alpha = 1.0
} else {
strongSelf.textNode.alpha = 0.0
}
}
if currentIsEmpty && !nextIsEmpty {
strongSelf.isHiddenInternal = false
if !strongSelf.disableBounce {
if bounce {
strongSelf.layer.animateScale(from: 0.0001, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.layer.animateScale(from: 1.15, to: 1.0, duration: 0.12, removeOnCompletion: false)
}
})
} else {
strongSelf.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.2, removeOnCompletion: false)
}
}
} else if !currentIsEmpty && !nextIsEmpty && currentContent?.text != content.text {
var animateScale = bounce
strongSelf.isHiddenInternal = false
if let currentText = currentContent?.text, let currentValue = Int(currentText), let text = content.text, let value = Int(text) {
if value < currentValue {
animateScale = false
}
}
if animateScale && !strongSelf.disableBounce {
strongSelf.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false)
}
})
}
var animateSnapshot = true
if let currentContent = currentContent, case .blank = currentContent {
animateSnapshot = false
}
if animateSnapshot, let snapshotView = strongSelf.textNode.view.snapshotContentTree() {
snapshotView.frame = strongSelf.textNode.frame
strongSelf.textNode.view.superview?.insertSubview(snapshotView, aboveSubview: strongSelf.textNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (badgeWidth - previousBadgeWidth) / 2.0, y: -8.0), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
animateTextNode = true
} else if !currentIsEmpty && nextIsEmpty && !strongSelf.isHiddenInternal {
strongSelf.isHiddenInternal = true
if !strongSelf.disableBounce {
strongSelf.layer.animateScale(from: 1.0, to: 0.0001, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.isHidden = true
strongSelf.layer.removeAnimation(forKey: "transform.scale")
}
})
} else {
strongSelf.isHidden = true
}
}
} else {
if case .none = content {
strongSelf.isHidden = true
strongSelf.isHiddenInternal = true
} else {
strongSelf.isHidden = false
strongSelf.isHiddenInternal = false
}
if case .text = content {
strongSelf.textNode.alpha = 1.0
} else {
strongSelf.textNode.alpha = 0.0
}
}
let _ = textLayoutAndApply?.1()
let backgroundFrame = CGRect(x: 0.0, y: 0.0, width: badgeWidth, height: strongSelf.backgroundNode.image?.size.height ?? 0.0)
if let (textLayout, _) = textLayoutAndApply {
let badgeTextFrame = CGRect(origin: CGPoint(x: backgroundFrame.midX - textLayout.size.width / 2.0, y: backgroundFrame.minY + UIScreenPixel + floorToScreenPixels((backgroundFrame.height - textLayout.size.height) / 2.0)), size: textLayout.size)
strongSelf.textNode.position = badgeTextFrame.center
strongSelf.textNode.bounds = CGRect(origin: CGPoint(), size: badgeTextFrame.size)
if animateTextNode {
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
strongSelf.textNode.layer.animatePosition(from: CGPoint(x: (previousBadgeWidth - badgeWidth) / 2.0, y: 8.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
}
strongSelf.backgroundNode.position = backgroundFrame.center
strongSelf.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size)
if animated && badgeWidth != previousBadgeWidth {
let previousBackgroundFrame = CGRect(x: 0.0, y: 0.0, width: previousBadgeWidth, height: backgroundFrame.height)
strongSelf.backgroundNode.layer.animateFrame(from: previousBackgroundFrame, to: backgroundFrame, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
}
})
}
}
}
@@ -0,0 +1,81 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import ListSectionHeaderNode
import AppBundle
class ChatListEmptyHeaderItem: ListViewItem {
let selectable: Bool = false
init() {
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatListEmptyHeaderItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply()
})
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ChatListEmptyHeaderItemNode)
if let nodeValue = node() as? ChatListEmptyHeaderItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem == nil)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
class ChatListEmptyHeaderItemNode: ListViewItemNode {
private var item: ChatListEmptyHeaderItem?
required init() {
super.init(layerBacked: false, dynamicBounce: false)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListEmptyHeaderItem, params, nextItem == nil)
apply()
}
func asyncLayout() -> (_ item: ChatListEmptyHeaderItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, last in
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 0.0), insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
}
})
}
}
}
@@ -0,0 +1,276 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import ListSectionHeaderNode
import AppBundle
import AnimatedStickerNode
import TelegramAnimatedStickerNode
class ChatListEmptyInfoItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let selectable: Bool = false
init(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
}
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 = ChatListEmptyInfoItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply()
})
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ChatListEmptyInfoItemNode)
if let nodeValue = node() as? ChatListEmptyInfoItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem == nil)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
class ChatListEmptyInfoItemNode: ListViewItemNode {
private var item: ChatListEmptyInfoItem?
private let animationNode: AnimatedStickerNode
private let textNode: TextNode
override var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = self.visibilityStatus
let isVisible: Bool
switch self.visibility {
case let .visible(fraction, _):
isVisible = fraction > 0.2
case .none:
isVisible = false
}
if wasVisible != isVisible {
self.visibilityStatus = isVisible
}
}
}
private var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
self.animationNode.visibility = self.visibilityStatus
}
}
}
required init() {
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.textNode = TextNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
}
override func didLoad() {
super.didLoad()
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListEmptyInfoItem, params, nextItem == nil)
apply()
}
func asyncLayout() -> (_ item: ChatListEmptyInfoItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let makeTextLayout = TextNode.asyncLayout(self.textNode)
return { item, params, last in
let baseWidth = params.width - params.leftInset - params.rightInset
let topInset: CGFloat = 8.0
let textSpacing: CGFloat = 27.0
let bottomInset: CGFloat = 24.0
let animationHeight: CGFloat = 140.0
let string = NSMutableAttributedString(string: item.strings.ChatList_EmptyChatList, font: Font.semibold(17.0), textColor: item.theme.list.itemPrimaryTextColor)
let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: string, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: .greatestFiniteMagnitude), alignment: .center))
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: topInset + animationHeight + textSpacing + textLayout.0.size.height + bottomInset), insets: UIEdgeInsets())
return (layout, { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.item = item
var topOffset: CGFloat = topInset
let animationFrame = CGRect(origin: CGPoint(x: floor((params.width - animationHeight) * 0.5), y: topOffset), size: CGSize(width: animationHeight, height: animationHeight))
if strongSelf.animationNode.bounds.isEmpty {
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListEmpty"), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
}
strongSelf.animationNode.frame = animationFrame
topOffset += animationHeight + textSpacing
let _ = textLayout.1()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: topOffset), size: textLayout.0.size)
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
})
}
}
}
class ChatListSectionHeaderItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let hide: (() -> Void)?
let selectable: Bool = false
init(theme: PresentationTheme, strings: PresentationStrings, hide: (() -> Void)?) {
self.theme = theme
self.strings = strings
self.hide = hide
}
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 = ChatListSectionHeaderNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply()
})
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ChatListSectionHeaderNode)
if let nodeValue = node() as? ChatListSectionHeaderNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem == nil)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
class ChatListSectionHeaderNode: ListViewItemNode {
private var item: ChatListSectionHeaderItem?
private var headerNode: ListSectionHeaderNode?
required init() {
super.init(layerBacked: false, dynamicBounce: false)
self.zPosition = 1.0
}
override func didLoad() {
super.didLoad()
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListSectionHeaderItem, params, nextItem == nil)
apply()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let headerNode = self.headerNode {
if let result = headerNode.view.hitTest(self.view.convert(point, to: headerNode.view), with: event) {
return result
}
}
return nil
}
func asyncLayout() -> (_ item: ChatListSectionHeaderItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, last in
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 28.0), insets: UIEdgeInsets())
return (layout, { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.item = item
let headerNode: ListSectionHeaderNode
if let current = strongSelf.headerNode {
headerNode = current
} else {
headerNode = ListSectionHeaderNode(theme: item.theme)
strongSelf.headerNode = headerNode
strongSelf.addSubnode(headerNode)
}
headerNode.title = item.strings.ChatList_EmptyListContactsHeader
if item.hide != nil {
headerNode.action = item.strings.ChatList_EmptyListContactsHeaderHide
headerNode.actionType = .generic
headerNode.activateAction = { _ in
guard let self else {
return
}
self.item?.hide?()
}
} else {
headerNode.action = nil
}
headerNode.updateTheme(theme: item.theme)
headerNode.updateLayout(size: CGSize(width: params.width, height: layout.contentSize.height), leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
})
}
}
}
@@ -0,0 +1,339 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import ComponentFlow
import LottieComponent
class ChatListHoleItem: ListViewItem {
let theme: PresentationTheme
let selectable: Bool = false
init(theme: PresentationTheme) {
self.theme = theme
}
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 = ChatListHoleItemNode()
node.relativePosition = (first: previousItem == nil, last: nextItem == nil)
node.insets = ChatListItemNode.insets(first: false, last: false, firstWithHeader: false)
node.layoutForParams(params, item: self, previousItem: previousItem, nextItem: nextItem)
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ChatListHoleItemNode)
if let nodeValue = node() as? ChatListHoleItemNode {
let layout = nodeValue.asyncLayout()
async {
let first = previousItem == nil
let last = nextItem == nil
let (nodeLayout, apply) = layout(self, params, first, last)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
class ChatListHoleItemNode: ListViewItemNode {
var relativePosition: (first: Bool, last: Bool) = (false, false)
required init() {
super.init(layerBacked: false, dynamicBounce: false)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListHoleItem, params, self.relativePosition.first, self.relativePosition.last)
apply()
}
func asyncLayout() -> (_ item: ChatListHoleItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, first, last in
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 0.0), insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.relativePosition = (first, last)
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
}
})
}
}
}
class ChatListSearchEmptyFooterItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let searchQuery: String?
let searchAllMessages: (() -> Void)?
let header: ListViewItemHeader?
let selectable: Bool = false
init(theme: PresentationTheme, strings: PresentationStrings, header: ListViewItemHeader?, searchQuery: String?, searchAllMessages: (() -> Void)?) {
self.theme = theme
self.strings = strings
self.header = header
self.searchQuery = searchQuery
self.searchAllMessages = searchAllMessages
}
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 = ChatListSearchEmptyFooterItemNode()
let (layout, apply) = node.asyncLayout()(self, params)
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 {
assert(node() is ChatListSearchEmptyFooterItemNode)
if let nodeValue = node() as? ChatListSearchEmptyFooterItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
class ChatListSearchEmptyFooterItemNode: ListViewItemNode {
private let contentNode: ASDisplayNode
private let titleNode: TextNode
private let textNode: TextNode
private let searchAllMessagesButton: HighlightableButtonNode
private let searchAllMessagesTitle: TextNode
private let icon = ComponentView<Empty>()
private var item: ChatListSearchEmptyFooterItem?
required init() {
self.contentNode = ASDisplayNode()
self.titleNode = TextNode()
self.textNode = TextNode()
self.searchAllMessagesButton = HighlightableButtonNode()
self.searchAllMessagesTitle = TextNode()
self.searchAllMessagesTitle.isUserInteractionEnabled = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.titleNode)
self.contentNode.addSubnode(self.textNode)
self.contentNode.addSubnode(self.searchAllMessagesButton)
self.searchAllMessagesButton.addSubnode(self.searchAllMessagesTitle)
self.searchAllMessagesButton.addTarget(self, action: #selector(self.searchAllMessagesButtonPressed), forControlEvents: .touchUpInside)
self.wantsTrailingItemSpaceUpdates = true
}
@objc private func searchAllMessagesButtonPressed() {
self.item?.searchAllMessages?()
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListSearchEmptyFooterItem, params)
apply()
}
override func headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
override func updateTrailingItemSpace(_ trailingItemSpace: CGFloat, transition: ContainedViewLayoutTransition) {
var contentFrame = self.contentNode.frame
contentFrame.origin.y = max(0.0, floor(trailingItemSpace * 0.5))
self.contentNode.frame = contentFrame
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if let contentResult = self.contentNode.view.hitTest(self.view.convert(point, to: self.contentNode.view), with: event), contentResult === self.searchAllMessagesButton.view {
return contentResult
}
return result
}
func asyncLayout() -> (_ item: ChatListSearchEmptyFooterItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleNodeLayout = TextNode.asyncLayout(self.titleNode)
let makeTextNodeLayout = TextNode.asyncLayout(self.textNode)
let makeSearchAllMessagesTitleLayout = TextNode.asyncLayout(self.searchAllMessagesTitle)
return { [weak self] item, params in
let titleLayout = makeTitleNodeLayout(TextNodeLayoutArguments(
attributedString: NSAttributedString(string: item.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: item.theme.list.freeTextColor),
maximumNumberOfLines: 1,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0)
))
let textValue: String
if let searchQuery = item.searchQuery {
textValue = item.strings.ChatList_Search_NoResultsQueryDescription(searchQuery).string
} else {
textValue = item.strings.ChatList_Search_NoResults
}
let textLayout = makeTextNodeLayout(TextNodeLayoutArguments(
attributedString: NSAttributedString(string: textValue, font: Font.regular(16.0), textColor: item.theme.list.freeTextColor),
maximumNumberOfLines: 0,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0),
alignment: .center,
lineSpacing: 0.1
))
let searchAllMessagesTitleLayout = makeSearchAllMessagesTitleLayout(TextNodeLayoutArguments(
attributedString: NSAttributedString(string: item.strings.ChatList_EmptyResult_SearchInAll, font: Font.regular(17.0), textColor: item.theme.list.itemAccentColor),
maximumNumberOfLines: 1,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0)
))
var contentHeight: CGFloat = 0.0
let topInset: CGFloat = 40.0
let bottomInset: CGFloat = 10.0
let iconSpacing: CGFloat = 20.0
let titleSpacing: CGFloat = 6.0
let buttonSpacing: CGFloat = 14.0
let buttonInset: CGFloat = 11.0
let iconSize = CGSize(width: 128.0, height: 128.0)
contentHeight += topInset
contentHeight += iconSize.height
contentHeight += iconSpacing
contentHeight += titleLayout.0.size.height
contentHeight += titleSpacing
contentHeight += textLayout.0.size.height
if item.searchAllMessages != nil {
contentHeight += buttonSpacing
contentHeight += buttonInset
contentHeight += searchAllMessagesTitleLayout.0.size.height
contentHeight += buttonInset
}
contentHeight += bottomInset
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: UIEdgeInsets())
return (layout, { [weak self] in
guard let self else {
return
}
self.item = item
self.contentSize = layout.contentSize
self.insets = layout.insets
let _ = titleLayout.1()
let _ = textLayout.1()
let _ = searchAllMessagesTitleLayout.1()
var contentY: CGFloat = 0.0
contentY += topInset
let _ = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(
name: "ChatListNoResults"
),
color: nil,
placeholderColor: nil,
startingPosition: .begin,
size: iconSize,
renderingScale: nil,
loop: false,
playOnce: nil
)),
environment: {}, containerSize: iconSize
)
let iconFrame = CGRect(origin: CGPoint(x: floor((params.width - iconSize.width) * 0.5), y: contentY), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.contentNode.view.addSubview(iconView)
}
iconView.frame = iconFrame
}
contentY += iconSize.height
contentY += iconSpacing
let titleFrame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: contentY), size: titleLayout.0.size)
self.titleNode.frame = titleFrame
contentY += titleLayout.0.size.height
contentY += titleSpacing
let textFrame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: contentY), size: textLayout.0.size)
self.textNode.frame = textFrame
contentY += textLayout.0.size.height
if item.searchAllMessages != nil {
contentY += buttonSpacing
let searchAllMessagesButtonFrame = CGRect(origin: CGPoint(x: floor((params.width - searchAllMessagesTitleLayout.0.size.width) * 0.5), y: contentY), size: CGSize(width: searchAllMessagesTitleLayout.0.size.width, height: searchAllMessagesTitleLayout.0.size.height + buttonInset * 2.0))
contentY += searchAllMessagesTitleLayout.0.size.height + buttonInset * 2.0
self.searchAllMessagesButton.frame = searchAllMessagesButtonFrame
self.searchAllMessagesTitle.frame = CGRect(origin: CGPoint(x: 0.0, y: buttonInset), size: searchAllMessagesTitleLayout.0.size)
contentY += buttonInset
contentY += searchAllMessagesTitleLayout.0.size.height
contentY += buttonInset
}
contentY += bottomInset
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: self.contentNode.frame.minY), size: CGSize(width: params.width, height: contentHeight))
self.contentNode.frame = contentFrame
})
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,473 @@
import Foundation
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import LocalizedPeerData
import TextFormat
private enum MessageGroupType {
case photos
case videos
case music
case files
case generic
}
private func singleMessageType(message: EngineMessage) -> MessageGroupType {
for media in message.media {
if let _ = media as? TelegramMediaImage {
return .photos
} else if let file = media as? TelegramMediaFile {
if file.isMusic {
return .music
}
if file.isVideo && !file.isInstantVideo {
return .videos
}
return .files
}
}
return .generic
}
private func singleExtendedMediaType(extendedMedia: TelegramExtendedMedia) -> MessageGroupType {
switch extendedMedia {
case let .preview(_, _, videoDuration):
if let _ = videoDuration {
return .videos
} else {
return .photos
}
case let .full(fullMedia):
if let _ = fullMedia as? TelegramMediaImage {
return .photos
} else if let file = fullMedia as? TelegramMediaFile, file.isVideo {
return .videos
}
}
return .generic
}
private func messageGroupType(messages: [EngineMessage]) -> MessageGroupType {
if messages.isEmpty {
return .generic
}
let currentType = singleMessageType(message: messages[0])
for i in 1 ..< messages.count {
let nextType = singleMessageType(message: messages[i])
if nextType != currentType {
return .generic
}
}
return currentType
}
private func paidContentGroupType(paidContent: TelegramMediaPaidContent) -> MessageGroupType {
if paidContent.extendedMedia.isEmpty {
return .generic
}
let currentType = singleExtendedMediaType(extendedMedia: paidContent.extendedMedia[0])
for i in 1 ..< paidContent.extendedMedia.count {
let nextType = singleExtendedMediaType(extendedMedia: paidContent.extendedMedia[i])
if nextType != currentType {
return .generic
}
}
return currentType
}
public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, contentSettings: ContentSettings, messages: [EngineMessage], chatPeer: EngineRenderedPeer, accountPeerId: EnginePeer.Id, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: EnginePeer?, hideAuthor: Bool, messageText: String, spoilers: [NSRange]?, customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)]?) {
let peer: EnginePeer?
let message = messages.last
if let restrictionReason = message?._asMessage().restrictionReason(platform: "ios", contentSettings: contentSettings) {
return (nil, false, restrictionReason, nil, nil)
}
if let restrictionReason = chatPeer.chatMainPeer?.restrictionText(platform: "ios", contentSettings: contentSettings) {
return (nil, false, restrictionReason, nil, nil)
}
var hideAuthor = false
var messageText: String
var spoilers: [NSRange]?
var customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)]?
if let message = message {
if let messageMain = messageMainPeer(message) {
peer = messageMain
} else {
peer = chatPeer.chatMainPeer
}
messageText = ""
for message in messages {
if !message.text.isEmpty {
messageText = message.text
break
}
}
let paidContent = message.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent
var textIsReady = false
if messages.count > 1 || (paidContent != nil && (paidContent?.extendedMedia.count ?? 0) > 1) {
let groupType: MessageGroupType
let count: Int32
if let paidContent {
groupType = paidContentGroupType(paidContent: paidContent)
count = Int32(paidContent.extendedMedia.count)
} else {
groupType = messageGroupType(messages: messages)
count = Int32(messages.count)
}
switch groupType {
case .photos:
if !messageText.isEmpty {
textIsReady = true
} else {
messageText = strings.ChatList_MessagePhotos(count)
textIsReady = true
}
case .videos:
if !messageText.isEmpty {
textIsReady = true
} else {
messageText = strings.ChatList_MessageVideos(count)
textIsReady = true
}
case .music:
if !messageText.isEmpty {
textIsReady = true
} else {
messageText = strings.ChatList_MessageMusic(count)
textIsReady = true
}
case .files:
if !messageText.isEmpty {
textIsReady = true
} else {
messageText = strings.ChatList_MessageFiles(count)
textIsReady = true
}
case .generic:
var messageTypes = Set<MessageGroupType>()
if let paidContent {
for extendedMedia in paidContent.extendedMedia {
messageTypes.insert(singleExtendedMediaType(extendedMedia: extendedMedia))
}
} else {
for message in messages {
messageTypes.insert(singleMessageType(message: message))
}
}
if messageTypes.count == 2 && messageTypes.contains(.photos) && messageTypes.contains(.videos) {
if !messageText.isEmpty {
textIsReady = true
}
}
}
}
if !textIsReady {
for media in message.media {
switch media {
case let paidContent as TelegramMediaPaidContent:
for extendedMedia in paidContent.extendedMedia {
let type = singleExtendedMediaType(extendedMedia: extendedMedia)
switch type {
case .photos:
if message.text.isEmpty {
messageText = strings.Message_Photo
} else if enableMediaEmoji {
messageText = "🖼 \(messageText)"
}
case .videos:
if message.text.isEmpty {
messageText = strings.Message_Video
} else if enableMediaEmoji {
messageText = "📹 \(messageText)"
}
default:
break
}
}
case _ as TelegramMediaImage:
if message.text.isEmpty {
messageText = strings.Message_Photo
} else if enableMediaEmoji {
messageText = "🖼 \(messageText)"
}
case let fileMedia as TelegramMediaFile:
var processed = false
inner: for attribute in fileMedia.attributes {
switch attribute {
case .Animated:
messageText = strings.Message_Animation
processed = true
break inner
case let .Audio(isVoice, _, title, performer, _):
if !message.text.isEmpty {
messageText = "🎤 \(messageText)"
processed = true
} else if isVoice {
if message.text.isEmpty {
messageText = strings.Message_Audio
} else {
messageText = "🎤 \(messageText)"
}
processed = true
break inner
} else {
let descriptionString: String
if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty {
descriptionString = title + "" + performer
} else if let title = title, !title.isEmpty {
descriptionString = title
} else if let performer = performer, !performer.isEmpty {
descriptionString = performer
} else if let fileName = fileMedia.fileName {
descriptionString = fileName
} else {
descriptionString = strings.Message_Audio
}
messageText = descriptionString
processed = true
break inner
}
case let .Sticker(displayText, _, _):
if displayText.isEmpty {
messageText = strings.Message_Sticker
processed = true
break inner
} else {
messageText = strings.Message_StickerText(displayText).string
processed = true
break inner
}
case let .Video(_, _, flags, _, _, _):
if flags.contains(.instantRoundVideo) {
messageText = strings.Message_VideoMessage
processed = true
break inner
} else {
if message.text.isEmpty {
messageText = strings.Message_Video
processed = true
} else {
if enableMediaEmoji {
if !fileMedia.isAnimated {
messageText = "📹 \(messageText)"
}
}
processed = true
break inner
}
}
default:
break
}
}
if !processed {
if !message.text.isEmpty {
messageText = "📎 \(messageText)"
} else {
if fileMedia.isAnimatedSticker {
messageText = strings.Message_Sticker
} else {
if let fileName = fileMedia.fileName {
messageText = fileName
} else {
messageText = strings.Message_File
}
}
}
}
case let location as TelegramMediaMap:
if location.liveBroadcastingTimeout != nil {
messageText = strings.Message_LiveLocation
} else {
messageText = strings.Message_Location
}
case _ as TelegramMediaContact:
messageText = strings.Message_Contact
case let game as TelegramMediaGame:
messageText = "🎮 \(game.title)"
case let invoice as TelegramMediaInvoice:
messageText = invoice.title
case let action as TelegramMediaAction:
switch action.action {
case let .conferenceCall(conferenceCall):
let incoming = message.flags.contains(.Incoming)
let missedTimeout: Int32 = 30
let currentTime = Int32(Date().timeIntervalSince1970)
if conferenceCall.flags.contains(.isMissed) {
messageText = strings.Chat_CallMessage_DeclinedGroupCall
} else if conferenceCall.duration == nil && message.timestamp < currentTime - missedTimeout {
messageText = strings.Chat_CallMessage_MissedGroupCall
} else {
if incoming {
messageText = strings.Chat_CallMessage_IncomingGroupCall
} else {
messageText = strings.Chat_CallMessage_OutgoingGroupCall
}
}
case let .phoneCall(_, discardReason, _, isVideo):
hideAuthor = !isPeerGroup
let incoming = message.flags.contains(.Incoming)
if let discardReason = discardReason {
switch discardReason {
case .disconnect:
if isVideo {
messageText = strings.Notification_VideoCallCanceled
} else {
messageText = strings.Notification_CallCanceled
}
case .missed, .busy:
if incoming {
if isVideo {
messageText = strings.Notification_VideoCallMissed
} else {
messageText = strings.Notification_CallMissed
}
} else {
if isVideo {
messageText = strings.Notification_VideoCallCanceled
} else {
messageText = strings.Notification_CallCanceled
}
}
case .hangup:
break
}
}
if messageText.isEmpty {
if incoming {
if isVideo {
messageText = strings.Notification_VideoCallIncoming
} else {
messageText = strings.Notification_CallIncoming
}
} else {
if isVideo {
messageText = strings.Notification_VideoCallOutgoing
} else {
messageText = strings.Notification_CallOutgoing
}
}
}
default:
switch action.action {
case .topicCreated, .topicEdited:
hideAuthor = false
default:
hideAuthor = true
}
if let (text, textSpoilers, customEmojiRangesValue) = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true, forForumOverview: false) {
messageText = text
spoilers = textSpoilers
customEmojiRanges = customEmojiRangesValue
}
}
case _ as TelegramMediaExpiredContent:
if let (text, _, _) = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true, forForumOverview: false) {
messageText = text
}
case let poll as TelegramMediaPoll:
let pollPrefix = "📊 "
let entityOffset = (pollPrefix as NSString).length
messageText = "\(pollPrefix)\(poll.text)"
for entity in poll.textEntities {
if case let .CustomEmoji(_, fileId) = entity.type {
if customEmojiRanges == nil {
customEmojiRanges = []
}
let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
customEmojiRanges?.append((range, attribute))
}
}
case let dice as TelegramMediaDice:
messageText = dice.emoji
case let story as TelegramMediaStory:
if story.isMention, let peer {
if message.flags.contains(.Incoming) {
messageText = strings.Conversation_StoryMentionTextIncoming(peer.compactDisplayTitle).string
} else {
messageText = strings.Conversation_StoryMentionTextOutgoing(peer.compactDisplayTitle).string
}
} else {
messageText = strings.Notification_Story
}
case _ as TelegramMediaGiveaway:
if let forwardInfo = message.forwardInfo, let author = forwardInfo.author {
messageText = strings.Message_GiveawayStartedOther(EnginePeer(author).compactDisplayTitle).string
} else {
if let author = message.author, case let .channel(channel) = author, case .group = channel.info {
messageText = strings.Message_GiveawayStartedGroup
} else {
messageText = strings.Message_GiveawayStarted
}
}
case let results as TelegramMediaGiveawayResults:
if results.winnersCount == 0 {
messageText = strings.Message_GiveawayEndedNoWinners
} else {
messageText = strings.Message_GiveawayEndedWinners(results.winnersCount)
}
case let webpage as TelegramMediaWebpage:
if messageText.isEmpty, case let .Loaded(content) = webpage.content {
messageText = content.displayUrl
}
case let todo as TelegramMediaTodo:
let pollPrefix = "☑️ "
let entityOffset = (pollPrefix as NSString).length
messageText = "\(pollPrefix)\(todo.text)"
for entity in todo.textEntities {
if case let .CustomEmoji(_, fileId) = entity.type {
if customEmojiRanges == nil {
customEmojiRanges = []
}
let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
customEmojiRanges?.append((range, attribute))
}
}
default:
break
}
}
}
} else {
peer = chatPeer.chatMainPeer
messageText = ""
if chatPeer.peerId.namespace == Namespaces.Peer.SecretChat {
if case let .secretChat(secretChat) = chatPeer.peers[chatPeer.peerId] {
switch secretChat.embeddedState {
case .active:
switch secretChat.role {
case .creator:
messageText = strings.DialogList_EncryptedChatStartedOutgoing(peer?.compactDisplayTitle ?? "").string
case .participant:
messageText = strings.DialogList_EncryptedChatStartedIncoming(peer?.compactDisplayTitle ?? "").string
}
case .terminated:
messageText = strings.DialogList_EncryptionRejected
case .handshake:
switch secretChat.role {
case .creator:
messageText = strings.DialogList_AwaitingEncryption(peer?.compactDisplayTitle ?? "").string
case .participant:
messageText = strings.DialogList_EncryptionProcessing
}
}
}
}
}
return (peer, hideAuthor, messageText, spoilers, customEmojiRanges)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,965 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import TelegramPresentationData
import MergeLists
import AccountContext
enum ChatListNodeEntryId: Hashable {
case Header
case Hole(Int64)
case PeerId(Int64)
case ThreadId(Int64)
case GroupId(EngineChatList.Group)
case ContactId(EnginePeer.Id)
case ArchiveIntro
case EmptyIntro
case SectionHeader
case Notice
case additionalCategory(Int)
}
enum ChatListNodeEntrySortIndex: Comparable {
case index(EngineChatList.Item.Index)
case additionalCategory(Int)
case sectionHeader
case contact(id: EnginePeer.Id, presence: EnginePeer.Presence)
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 .sectionHeader:
return true
case .contact:
return true
}
case let .additionalCategory(lhsIndex):
switch rhs {
case let .additionalCategory(rhsIndex):
return lhsIndex < rhsIndex
case .index:
return true
case .sectionHeader:
return true
case .contact:
return true
}
case .sectionHeader:
switch rhs {
case .additionalCategory, .index, .sectionHeader:
return false
case .contact:
return true
}
case let .contact(lhsId, lhsPresense):
switch rhs {
case .sectionHeader:
return false
case let .contact(rhsId, rhsPresense):
if lhsPresense != rhsPresense {
return rhsPresense.status > rhsPresense.status
} else {
return lhsId < rhsId
}
default:
return false
}
}
}
}
public enum ChatListNodeEntryPromoInfo: Equatable {
case proxy
case psa(type: String, message: String?)
}
public enum ChatListNotice: Equatable {
case clearStorage(sizeFraction: Double)
case setupPassword
case premiumUpgrade(discount: Int32)
case premiumAnnualDiscount(discount: Int32)
case premiumRestore(discount: Int32)
case xmasPremiumGift
case setupBirthday
case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday])
case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int)
case premiumGrace
case starsSubscriptionLowBalance(amount: StarsAmount, peers: [EnginePeer])
case setupPhoto(EnginePeer)
case accountFreeze
case link(id: String, url: String, title: ServerSuggestionInfo.Item.Text, subtitle: ServerSuggestionInfo.Item.Text)
}
enum ChatListNodeEntry: Comparable, Identifiable {
struct PeerEntryData: Equatable {
var index: EngineChatList.Item.Index
var presentationData: ChatListPresentationData
var messages: [EngineMessage]
var readState: EnginePeerReadCounters?
var isRemovedFromTotalUnreadCount: Bool
var draftState: ChatListItemContent.DraftState?
var mediaDraftContentType: EngineChatList.MediaDraftContentType?
var peer: EngineRenderedPeer
var threadInfo: ChatListItemContent.ThreadInfo?
var presence: EnginePeer.Presence?
var hasUnseenMentions: Bool
var hasUnseenReactions: Bool
var editing: Bool
var hasActiveRevealControls: Bool
var selected: Bool
var inputActivities: [(EnginePeer, PeerInputActivity)]?
var promoInfo: ChatListNodeEntryPromoInfo?
var hasFailedMessages: Bool
var isContact: Bool
var autoremoveTimeout: Int32?
var forumTopicData: EngineChatList.ForumTopicData?
var topForumTopicItems: [EngineChatList.ForumTopicData]
var revealed: Bool
var storyState: ChatListNodeState.StoryState?
var requiresPremiumForMessaging: Bool
var displayAsTopicList: Bool
init(
index: EngineChatList.Item.Index,
presentationData: ChatListPresentationData,
messages: [EngineMessage],
readState: EnginePeerReadCounters?,
isRemovedFromTotalUnreadCount: Bool,
draftState: ChatListItemContent.DraftState?,
mediaDraftContentType: EngineChatList.MediaDraftContentType?,
peer: EngineRenderedPeer,
threadInfo: ChatListItemContent.ThreadInfo?,
presence: EnginePeer.Presence?,
hasUnseenMentions: Bool,
hasUnseenReactions: Bool,
editing: Bool,
hasActiveRevealControls: Bool,
selected: Bool,
inputActivities: [(EnginePeer, PeerInputActivity)]?,
promoInfo: ChatListNodeEntryPromoInfo?,
hasFailedMessages: Bool,
isContact: Bool,
autoremoveTimeout: Int32?,
forumTopicData: EngineChatList.ForumTopicData?,
topForumTopicItems: [EngineChatList.ForumTopicData],
revealed: Bool,
storyState: ChatListNodeState.StoryState?,
requiresPremiumForMessaging: Bool,
displayAsTopicList: Bool
) {
self.index = index
self.presentationData = presentationData
self.messages = messages
self.readState = readState
self.isRemovedFromTotalUnreadCount = isRemovedFromTotalUnreadCount
self.draftState = draftState
self.mediaDraftContentType = mediaDraftContentType
self.peer = peer
self.threadInfo = threadInfo
self.presence = presence
self.hasUnseenMentions = hasUnseenMentions
self.hasUnseenReactions = hasUnseenReactions
self.editing = editing
self.hasActiveRevealControls = hasActiveRevealControls
self.selected = selected
self.inputActivities = inputActivities
self.promoInfo = promoInfo
self.hasFailedMessages = hasFailedMessages
self.isContact = isContact
self.autoremoveTimeout = autoremoveTimeout
self.forumTopicData = forumTopicData
self.topForumTopicItems = topForumTopicItems
self.revealed = revealed
self.storyState = storyState
self.requiresPremiumForMessaging = requiresPremiumForMessaging
self.displayAsTopicList = displayAsTopicList
}
static func ==(lhs: PeerEntryData, rhs: PeerEntryData) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.presentationData !== rhs.presentationData {
return false
}
if lhs.readState != rhs.readState {
return false
}
if lhs.messages.count != rhs.messages.count {
return false
}
for i in 0 ..< lhs.messages.count {
if lhs.messages[i].stableVersion != rhs.messages[i].stableVersion {
return false
}
if lhs.messages[i].id != rhs.messages[i].id {
return false
}
if lhs.messages[i].associatedMessages.count != rhs.messages[i].associatedMessages.count {
return false
}
for (id, message) in lhs.messages[i].associatedMessages {
if let otherMessage = rhs.messages[i].associatedMessages[id] {
if message.stableVersion != otherMessage.stableVersion {
return false
}
} else {
return false
}
}
}
if lhs.isRemovedFromTotalUnreadCount != rhs.isRemovedFromTotalUnreadCount {
return false
}
if let lhsPeerPresence = lhs.presence, let rhsPeerPresence = rhs.presence {
if lhsPeerPresence != rhsPeerPresence {
return false
}
} else if (lhs.presence != nil) != (rhs.presence != nil) {
return false
}
if let lhsEmbeddedState = lhs.draftState, let rhsEmbeddedState = rhs.draftState {
if lhsEmbeddedState != rhsEmbeddedState {
return false
}
} else if (lhs.draftState != nil) != (rhs.draftState != nil) {
return false
}
if lhs.mediaDraftContentType != rhs.mediaDraftContentType {
return false
}
if lhs.editing != rhs.editing {
return false
}
if lhs.hasActiveRevealControls != rhs.hasActiveRevealControls {
return false
}
if lhs.selected != rhs.selected {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.threadInfo != rhs.threadInfo {
return false
}
if lhs.hasUnseenMentions != rhs.hasUnseenMentions {
return false
}
if lhs.hasUnseenReactions != rhs.hasUnseenReactions {
return false
}
if let lhsInputActivities = lhs.inputActivities, let rhsInputActivities = rhs.inputActivities {
if lhsInputActivities.count != rhsInputActivities.count {
return false
}
for i in 0 ..< lhsInputActivities.count {
if lhsInputActivities[i].0 != rhsInputActivities[i].0 {
return false
}
if lhsInputActivities[i].1 != rhsInputActivities[i].1 {
return false
}
}
} else if (lhs.inputActivities != nil) != (rhs.inputActivities != nil) {
return false
}
if lhs.promoInfo != rhs.promoInfo {
return false
}
if lhs.hasFailedMessages != rhs.hasFailedMessages {
return false
}
if lhs.isContact != rhs.isContact {
return false
}
if lhs.autoremoveTimeout != rhs.autoremoveTimeout {
return false
}
if lhs.forumTopicData != rhs.forumTopicData {
return false
}
if lhs.topForumTopicItems != rhs.topForumTopicItems {
return false
}
if lhs.revealed != rhs.revealed {
return false
}
if lhs.storyState != rhs.storyState {
return false
}
if lhs.requiresPremiumForMessaging != rhs.requiresPremiumForMessaging {
return false
}
if lhs.displayAsTopicList != rhs.displayAsTopicList {
return false
}
return true
}
}
struct ContactEntryData: Equatable {
var presentationData: ChatListPresentationData
var peer: EnginePeer
var presence: EnginePeer.Presence
init(presentationData: ChatListPresentationData, peer: EnginePeer, presence: EnginePeer.Presence) {
self.presentationData = presentationData
self.peer = peer
self.presence = presence
}
static func ==(lhs: ContactEntryData, rhs: ContactEntryData) -> Bool {
if lhs.presentationData !== rhs.presentationData {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.presence != rhs.presence {
return false
}
return true
}
}
struct GroupReferenceEntryData: Equatable {
var index: EngineChatList.Item.Index
var presentationData: ChatListPresentationData
var groupId: EngineChatList.Group
var peers: [EngineChatList.GroupItem.Item]
var message: EngineMessage?
var editing: Bool
var unreadCount: Int
var revealed: Bool
var hiddenByDefault: Bool
var storyState: ChatListNodeState.StoryState?
init(
index: EngineChatList.Item.Index,
presentationData: ChatListPresentationData,
groupId: EngineChatList.Group,
peers: [EngineChatList.GroupItem.Item],
message: EngineMessage?,
editing: Bool,
unreadCount: Int,
revealed: Bool,
hiddenByDefault: Bool,
storyState: ChatListNodeState.StoryState?
) {
self.index = index
self.presentationData = presentationData
self.groupId = groupId
self.peers = peers
self.message = message
self.editing = editing
self.unreadCount = unreadCount
self.revealed = revealed
self.hiddenByDefault = hiddenByDefault
self.storyState = storyState
}
static func ==(lhs: GroupReferenceEntryData, rhs: GroupReferenceEntryData) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.presentationData !== rhs.presentationData {
return false
}
if lhs.groupId != rhs.groupId {
return false
}
if lhs.peers != rhs.peers {
return false
}
if lhs.message?.stableId != rhs.message?.stableId {
return false
}
if lhs.editing != rhs.editing {
return false
}
if lhs.unreadCount != rhs.unreadCount {
return false
}
if lhs.revealed != rhs.revealed {
return false
}
if lhs.hiddenByDefault != rhs.hiddenByDefault {
return false
}
if lhs.storyState != rhs.storyState {
return false
}
return true
}
}
case HeaderEntry
case PeerEntry(PeerEntryData)
case HoleEntry(EngineMessage.Index, theme: PresentationTheme)
case GroupReferenceEntry(GroupReferenceEntryData)
case ContactEntry(ContactEntryData)
case ArchiveIntro(presentationData: ChatListPresentationData)
case EmptyIntro(presentationData: ChatListPresentationData)
case SectionHeader(presentationData: ChatListPresentationData, displayHide: Bool)
case Notice(presentationData: ChatListPresentationData, notice: ChatListNotice)
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData)
var sortIndex: ChatListNodeEntrySortIndex {
switch self {
case .HeaderEntry:
return .index(.chatList(.absoluteUpperBound))
case let .PeerEntry(peerEntry):
return .index(peerEntry.index)
case let .HoleEntry(holeIndex, _):
return .index(.chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: holeIndex)))
case let .GroupReferenceEntry(groupReferenceEntry):
return .index(groupReferenceEntry.index)
case let .ContactEntry(contactEntry):
return .contact(id: contactEntry.peer.id, presence: contactEntry.presence)
case .ArchiveIntro:
return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor))
case .EmptyIntro:
return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor))
case .SectionHeader:
return .sectionHeader
case .Notice:
return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor.successor))
case let .AdditionalCategory(index, _, _, _, _, _, _):
return .additionalCategory(index)
}
}
var stableId: ChatListNodeEntryId {
switch self {
case .HeaderEntry:
return .Header
case let .PeerEntry(peerEntry):
switch peerEntry.index {
case let .chatList(index):
return .PeerId(index.messageIndex.id.peerId.toInt64())
case let .forum(_, _, threadId, _, _):
return .ThreadId(threadId)
}
case let .HoleEntry(holeIndex, _):
return .Hole(Int64(holeIndex.id.id))
case let .GroupReferenceEntry(groupReferenceEntry):
return .GroupId(groupReferenceEntry.groupId)
case let .ContactEntry(contactEntry):
return .ContactId(contactEntry.peer.id)
case .ArchiveIntro:
return .ArchiveIntro
case .EmptyIntro:
return .EmptyIntro
case .SectionHeader:
return .SectionHeader
case .Notice:
return .Notice
case let .AdditionalCategory(_, id, _, _, _, _, _):
return .additionalCategory(id)
}
}
static func <(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
static func ==(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool {
switch lhs {
case .HeaderEntry:
if case .HeaderEntry = rhs {
return true
} else {
return false
}
case let .PeerEntry(peerEntry):
if case .PeerEntry(peerEntry) = rhs {
return true
} else {
return false
}
case let .HoleEntry(lhsHole, lhsTheme):
switch rhs {
case let .HoleEntry(rhsHole, rhsTheme):
return lhsHole == rhsHole && lhsTheme === rhsTheme
default:
return false
}
case let .GroupReferenceEntry(groupReferenceEntry):
if case .GroupReferenceEntry(groupReferenceEntry) = rhs {
return true
} else {
return false
}
case let .ContactEntry(contactEntry):
if case .ContactEntry(contactEntry) = rhs {
return true
} else {
return false
}
case let .ArchiveIntro(lhsPresentationData):
if case let .ArchiveIntro(rhsPresentationData) = rhs {
if lhsPresentationData !== rhsPresentationData {
return false
}
return true
} else {
return false
}
case let .EmptyIntro(lhsPresentationData):
if case let .EmptyIntro(rhsPresentationData) = rhs {
if lhsPresentationData !== rhsPresentationData {
return false
}
return true
} else {
return false
}
case let .SectionHeader(lhsPresentationData, lhsDisplayHide):
if case let .SectionHeader(rhsPresentationData, rhsDisplayHide) = rhs {
if lhsPresentationData !== rhsPresentationData {
return false
}
if lhsDisplayHide != rhsDisplayHide {
return false
}
return true
} else {
return false
}
case let .Notice(lhsPresentationData, lhsInfo):
if case let .Notice(rhsPresentationData, rhsInfo) = rhs {
if lhsPresentationData !== rhsPresentationData {
return false
}
if lhsInfo != rhsInfo {
return false
}
return true
} else {
return false
}
case let .AdditionalCategory(lhsIndex, lhsId, lhsTitle, lhsImage, lhsAppearance, lhsSelected, lhsPresentationData):
if case let .AdditionalCategory(rhsIndex, rhsId, rhsTitle, rhsImage, rhsAppearance, rhsSelected, rhsPresentationData) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsId != rhsId {
return false
}
if lhsTitle != rhsTitle {
return false
}
if lhsImage !== rhsImage {
return false
}
if lhsAppearance != rhsAppearance {
return false
}
if lhsSelected != rhsSelected {
return false
}
if lhsPresentationData !== rhsPresentationData {
return false
}
return true
} else {
return false
}
}
}
}
private func offsetPinnedIndex(_ index: EngineChatList.Item.Index, offset: UInt16) -> EngineChatList.Item.Index {
if case let .chatList(index) = index, let pinningIndex = index.pinningIndex {
return .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: pinningIndex + offset, messageIndex: index.messageIndex))
} else {
return index
}
}
struct ChatListContactPeer {
var peer: EnginePeer
var presence: EnginePeer.Presence
init(peer: EnginePeer, presence: EnginePeer.Presence) {
self.peer = peer
self.presence = presence
}
}
func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer], accountPeerId: EnginePeer.Id, isMainTab: Bool) -> (entries: [ChatListNodeEntry], loading: Bool) {
var groupItems = view.groupItems
if isMainTab && state.archiveStoryState != nil && groupItems.isEmpty {
groupItems.append(EngineChatList.GroupItem(
id: .archive,
topMessage: nil,
items: [],
unreadCount: 0
))
}
var result: [ChatListNodeEntry] = []
var hasContacts = false
if !view.hasEarlier {
var existingPeerIds = Set<EnginePeer.Id>()
for item in view.items {
existingPeerIds.insert(item.renderedPeer.peerId)
}
for contact in contacts {
if existingPeerIds.contains(contact.peer.id) {
continue
}
result.append(.ContactEntry(ChatListNodeEntry.ContactEntryData(
presentationData: state.presentationData,
peer: contact.peer,
presence: contact.presence
)))
hasContacts = true
}
if hasContacts {
result.append(.SectionHeader(presentationData: state.presentationData, displayHide: !view.items.isEmpty))
}
}
var pinnedIndexOffset: UInt16 = 0
if !view.hasLater, case .chatList = mode {
var groupEntryCount = 0
for _ in groupItems {
groupEntryCount += 1
}
pinnedIndexOffset += UInt16(groupEntryCount)
}
let filteredAdditionalItemEntries = view.additionalItems.filter { item -> Bool in
return item.item.renderedPeer.peerId != state.hiddenPsaPeerId
}
var foundPeerIds = Set<EnginePeer.Id>()
for peer in foundPeers {
foundPeerIds.insert(peer.0.id)
}
if !view.hasLater && savedMessagesPeer == nil {
pinnedIndexOffset += UInt16(filteredAdditionalItemEntries.count)
}
var hiddenGeneralThread: ChatListNodeEntry?
loop: for entry in view.items {
var peerId: EnginePeer.Id?
var threadId: Int64?
var activityItemId: ChatListNodePeerInputActivities.ItemId?
if case let .chatList(index) = entry.index {
peerId = index.messageIndex.id.peerId
activityItemId = ChatListNodePeerInputActivities.ItemId(peerId: index.messageIndex.id.peerId, threadId: nil)
} else if case let .forum(_, _, threadIdValue, _, _) = entry.index, case let .forum(peerIdValue) = chatListLocation {
peerId = peerIdValue
activityItemId = ChatListNodePeerInputActivities.ItemId(peerId: peerIdValue, threadId: threadIdValue)
threadId = threadIdValue
}
if let savedMessagesPeer = savedMessagesPeer, let peerId = peerId, savedMessagesPeer.id == peerId || foundPeerIds.contains(peerId) {
continue loop
}
if let peerId = peerId, state.pendingRemovalItemIds.contains(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) {
continue loop
}
var updatedMessages = entry.messages
var updatedCombinedReadState = entry.readCounters
if let peerId = peerId, state.pendingClearHistoryPeerIds.contains(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) {
updatedMessages = []
updatedCombinedReadState = nil
}
var draftState: ChatListItemContent.DraftState?
if let draft = entry.draft {
draftState = ChatListItemContent.DraftState(draft: draft)
}
var hasActiveRevealControls = false
if let peerId {
hasActiveRevealControls = ChatListNodeState.ItemId(peerId: peerId, threadId: threadId) == state.peerIdWithRevealedOptions
}
var inputActivities: [(EnginePeer, PeerInputActivity)]?
if let activityItemId {
inputActivities = state.peerInputActivities?.activities[activityItemId]
}
var isSelected = false
if let threadId, threadId != 0 {
isSelected = state.selectedThreadIds.contains(threadId)
} else if let peerId {
isSelected = state.selectedPeerIds.contains(peerId)
}
var threadInfo: ChatListItemContent.ThreadInfo?
if let threadData = entry.threadData, let threadId {
threadInfo = ChatListItemContent.ThreadInfo(id: threadId, info: threadData.info, isOwnedByMe: threadData.isOwnedByMe, isClosed: threadData.isClosed, isHidden: threadData.isHidden, threadPeer: nil)
}
let entry: ChatListNodeEntry = .PeerEntry(ChatListNodeEntry.PeerEntryData(
index: offsetPinnedIndex(entry.index, offset: pinnedIndexOffset),
presentationData: state.presentationData,
messages: updatedMessages,
readState: updatedCombinedReadState,
isRemovedFromTotalUnreadCount: entry.isMuted,
draftState: draftState,
mediaDraftContentType: entry.mediaDraftContentType,
peer: entry.renderedPeer,
threadInfo: threadInfo,
presence: entry.presence,
hasUnseenMentions: entry.hasUnseenMentions,
hasUnseenReactions: entry.hasUnseenReactions,
editing: state.editing,
hasActiveRevealControls: hasActiveRevealControls,
selected: isSelected,
inputActivities: inputActivities,
promoInfo: nil,
hasFailedMessages: entry.hasFailed,
isContact: entry.isContact,
autoremoveTimeout: entry.autoremoveTimeout,
forumTopicData: entry.forumTopicData,
topForumTopicItems: entry.topForumTopicItems,
revealed: threadId == 1 && (state.hiddenItemShouldBeTemporaryRevealed || state.editing),
storyState: entry.renderedPeer.peerId == accountPeerId ? nil : entry.storyStats.flatMap { stats -> ChatListNodeState.StoryState in
return ChatListNodeState.StoryState(
stats: stats,
hasUnseenCloseFriends: stats.hasUnseenCloseFriends
)
},
requiresPremiumForMessaging: entry.isPremiumRequiredToMessage,
displayAsTopicList: entry.displayAsTopicList
))
if let threadInfo, threadInfo.isHidden {
hiddenGeneralThread = entry
} else {
result.append(entry)
}
}
if let hiddenGeneralThread {
result.append(hiddenGeneralThread)
}
if !view.hasLater {
var pinningIndex: UInt16 = UInt16(pinnedIndexOffset == 0 ? 0 : (pinnedIndexOffset - 1))
if let savedMessagesPeer = savedMessagesPeer {
if !foundPeers.isEmpty {
var foundPinningIndex: UInt16 = UInt16(foundPeers.count)
for peer in foundPeers.reversed() {
var peers: [EnginePeer.Id: EnginePeer] = [peer.0.id: peer.0]
if let chatPeer = peer.1 {
peers[chatPeer.id] = chatPeer
}
let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: peer.0.id, namespace: 0, id: 0), timestamp: 1)
result.append(.PeerEntry(ChatListNodeEntry.PeerEntryData(
index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: foundPinningIndex, messageIndex: messageIndex)),
presentationData: state.presentationData,
messages: [],
readState: nil,
isRemovedFromTotalUnreadCount: false,
draftState: nil,
mediaDraftContentType: nil,
peer: EngineRenderedPeer(peerId: peer.0.id, peers: peers, associatedMedia: [:]),
threadInfo: nil,
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
editing: state.editing,
hasActiveRevealControls: false,
selected: state.selectedPeerIds.contains(peer.0.id),
inputActivities: nil,
promoInfo: nil,
hasFailedMessages: false,
isContact: false,
autoremoveTimeout: nil,
forumTopicData: nil,
topForumTopicItems: [],
revealed: false,
storyState: nil,
requiresPremiumForMessaging: false,
displayAsTopicList: false
)))
if foundPinningIndex != 0 {
foundPinningIndex -= 1
}
}
}
result.append(.PeerEntry(ChatListNodeEntry.PeerEntryData(
index: .chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.predecessor),
presentationData: state.presentationData,
messages: [],
readState: nil,
isRemovedFromTotalUnreadCount: false,
draftState: nil,
mediaDraftContentType: nil,
peer: EngineRenderedPeer(peerId: savedMessagesPeer.id, peers: [savedMessagesPeer.id: savedMessagesPeer], associatedMedia: [:]),
threadInfo: nil,
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
editing: state.editing,
hasActiveRevealControls: false,
selected: state.selectedPeerIds.contains(savedMessagesPeer.id),
inputActivities: nil,
promoInfo: nil,
hasFailedMessages: false,
isContact: false,
autoremoveTimeout: nil,
forumTopicData: nil,
topForumTopicItems: [],
revealed: false,
storyState: nil,
requiresPremiumForMessaging: false,
displayAsTopicList: false
)))
} else {
if !filteredAdditionalItemEntries.isEmpty {
for item in filteredAdditionalItemEntries.reversed() {
guard case let .chatList(index) = item.item.index else {
continue
}
let promoInfo: ChatListNodeEntryPromoInfo
switch item.promoInfo.content {
case .proxy:
promoInfo = .proxy
case let .psa(type, message):
promoInfo = .psa(type: type, message: message)
}
let draftState = item.item.draft.flatMap(ChatListItemContent.DraftState.init)
let peerId = index.messageIndex.id.peerId
let isSelected = state.selectedPeerIds.contains(peerId)
var threadId: Int64 = 0
switch item.item.index {
case let .forum(_, _, threadIdValue, _, _):
threadId = threadIdValue
default:
break
}
result.append(.PeerEntry(ChatListNodeEntry.PeerEntryData(
index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: pinningIndex, messageIndex: index.messageIndex)),
presentationData: state.presentationData,
messages: item.item.messages,
readState: item.item.readCounters,
isRemovedFromTotalUnreadCount: item.item.isMuted,
draftState: draftState,
mediaDraftContentType: item.item.mediaDraftContentType,
peer: item.item.renderedPeer,
threadInfo: item.item.threadData.flatMap {
return ChatListItemContent.ThreadInfo(id: threadId, info: $0.info, isOwnedByMe: $0.isOwnedByMe, isClosed: $0.isClosed, isHidden: $0.isHidden, threadPeer: nil)
},
presence: item.item.presence,
hasUnseenMentions: item.item.hasUnseenMentions,
hasUnseenReactions: item.item.hasUnseenReactions,
editing: state.editing,
hasActiveRevealControls: ChatListNodeState.ItemId(peerId: peerId, threadId: threadId) == state.peerIdWithRevealedOptions,
selected: isSelected,
inputActivities: state.peerInputActivities?.activities[ChatListNodePeerInputActivities.ItemId(peerId: peerId, threadId: nil)],
promoInfo: promoInfo,
hasFailedMessages: item.item.hasFailed,
isContact: item.item.isContact,
autoremoveTimeout: item.item.autoremoveTimeout,
forumTopicData: item.item.forumTopicData,
topForumTopicItems: item.item.topForumTopicItems,
revealed: state.hiddenItemShouldBeTemporaryRevealed || state.editing,
storyState: nil,
requiresPremiumForMessaging: false,
displayAsTopicList: false
)))
if pinningIndex != 0 {
pinningIndex -= 1
}
}
}
}
if !view.hasLater, case .chatList = mode {
for groupReference in groupItems {
let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: EnginePeer.Id(0), namespace: 0, id: 0), timestamp: 1)
var mappedStoryState: ChatListNodeState.StoryState?
if let archiveStoryState = state.archiveStoryState {
mappedStoryState = archiveStoryState
}
result.append(.GroupReferenceEntry(ChatListNodeEntry.GroupReferenceEntryData(
index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: pinningIndex, messageIndex: messageIndex)),
presentationData: state.presentationData,
groupId: groupReference.id,
peers: groupReference.items,
message: groupReference.topMessage,
editing: state.editing,
unreadCount: groupReference.unreadCount,
revealed: state.hiddenItemShouldBeTemporaryRevealed,
hiddenByDefault: hideArchivedFolderByDefault,
storyState: mappedStoryState
)))
if pinningIndex != 0 {
pinningIndex -= 1
}
}
if displayArchiveIntro {
//result.append(.ArchiveIntro(presentationData: state.presentationData))
} else if !contacts.isEmpty && !result.contains(where: { entry in
if case .PeerEntry = entry {
return true
} else {
return false
}
}) {
result.append(.EmptyIntro(presentationData: state.presentationData))
}
if let notice {
result.append(.Notice(presentationData: state.presentationData, notice: notice))
}
result.append(.HeaderEntry)
}
if !view.hasLater {
if 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, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
index += 1
}
} else if case let .peerType(types, hasCreate) = mode, !result.isEmpty && hasCreate {
for type in types {
switch type {
case .group:
result.append(.AdditionalCategory(index: 0, id: 0, title: state.presentationData.strings.RequestPeer_CreateNewGroup, image: PresentationResourcesItemList.createGroupIcon(state.presentationData.theme), appearance: .action, selected: false, presentationData: state.presentationData))
case .channel:
result.append(.AdditionalCategory(index: 0, id: 0, title: state.presentationData.strings.RequestPeer_CreateNewChannel, image: PresentationResourcesItemList.createGroupIcon(state.presentationData.theme), appearance: .action, selected: false, presentationData: state.presentationData))
default:
break
}
}
}
}
}
if result.count >= 1, case .HoleEntry = result[result.count - 1] {
return ([.HeaderEntry], true)
} else if result.count == 1, case .HoleEntry = result[0] {
return ([.HeaderEntry], true)
}
return (result, view.isLoading)
}
@@ -0,0 +1,419 @@
import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
import TelegramUIPreferences
import AccountContext
public enum ChatListNodeLocation: Equatable {
case initial(count: Int, filter: ChatListFilter?)
case navigation(index: EngineChatList.Item.Index, filter: ChatListFilter?)
case scroll(index: EngineChatList.Item.Index, sourceIndex: EngineChatList.Item.Index, scrollPosition: ListViewScrollPosition, animated: Bool, filter: ChatListFilter?)
public var filter: ChatListFilter? {
switch self {
case let .initial(_, filter):
return filter
case let .navigation(_, filter):
return filter
case let .scroll(_, _, _, _, filter):
return filter
}
}
}
public struct ChatListNodeViewUpdate {
public let list: EngineChatList
public let type: ViewUpdateType
public let scrollPosition: ChatListNodeViewScrollPosition?
public init(list: EngineChatList, type: ViewUpdateType, scrollPosition: ChatListNodeViewScrollPosition?) {
self.list = list
self.type = type
self.scrollPosition = scrollPosition
}
}
public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: EnginePeer.Id) -> ChatListFilterPredicate {
var includePeers = Set(filter.includePeers.peers)
var excludePeers = Set(filter.excludePeers)
if !filter.includePeers.pinnedPeers.isEmpty {
includePeers.subtract(filter.includePeers.pinnedPeers)
excludePeers.subtract(filter.includePeers.pinnedPeers)
}
var includeAdditionalPeerGroupIds: [PeerGroupId] = []
if !filter.excludeArchived {
includeAdditionalPeerGroupIds.append(Namespaces.PeerGroup.archive)
}
var messageTagSummary: ChatListMessageTagSummaryResultCalculation?
if filter.excludeRead || filter.excludeMuted {
messageTagSummary = ChatListMessageTagSummaryResultCalculation(addCount: ChatListMessageTagSummaryResultComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), subtractCount: ChatListMessageTagActionsSummaryResultComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud))
}
return ChatListFilterPredicate(includePeerIds: includePeers, excludePeerIds: excludePeers, pinnedPeerIds: filter.includePeers.pinnedPeers, messageTagSummary: messageTagSummary, includeAdditionalPeerGroupIds: includeAdditionalPeerGroupIds, include: { peer, isMuted, isUnread, isContact, messageTagSummaryResult in
if filter.excludeRead {
var effectiveUnread = isUnread
if let messageTagSummaryResult = messageTagSummaryResult, messageTagSummaryResult {
effectiveUnread = true
}
if !effectiveUnread {
return false
}
}
if filter.excludeMuted {
if isMuted {
if let messageTagSummaryResult = messageTagSummaryResult, messageTagSummaryResult {
} else {
return false
}
}
}
if !filter.categories.contains(.contacts) && isContact {
if let user = peer as? TelegramUser {
if user.botInfo == nil && !user.flags.contains(.isSupport) {
return false
}
} else if let _ = peer as? TelegramSecretChat {
return false
}
}
if !filter.categories.contains(.nonContacts) && (!isContact && peer.id != accountPeerId) {
if let user = peer as? TelegramUser {
if user.botInfo == nil {
return false
}
} else if let _ = peer as? TelegramSecretChat {
return false
}
}
if filter.categories.contains(.nonContacts) && peer.id == accountPeerId {
return false
}
if !filter.categories.contains(.bots) {
if let user = peer as? TelegramUser {
if user.botInfo != nil || user.flags.contains(.isSupport) {
return false
}
}
}
if !filter.categories.contains(.groups) {
if let _ = peer as? TelegramGroup {
return false
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
return false
}
}
}
if !filter.categories.contains(.channels) {
if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
return false
}
}
}
return true
})
}
public func chatListViewForLocation(chatListLocation: ChatListControllerLocation, location: ChatListNodeLocation, account: Account, shouldLoadCanMessagePeer: Bool) -> Signal<ChatListNodeViewUpdate, NoError> {
let accountPeerId = account.peerId
switch chatListLocation {
case let .chatList(groupId):
let filterPredicate: ChatListFilterPredicate?
if let filter = location.filter, case let .filter(_, _, _, data) = filter {
filterPredicate = chatListFilterPredicate(filter: data, accountPeerId: account.peerId)
} else {
filterPredicate = nil
}
switch location {
case let .initial(count, _):
let signal: Signal<(ChatListView, ViewUpdateType), NoError>
signal = account.viewTracker.tailChatListView(groupId: groupId._asGroup(), filterPredicate: filterPredicate, count: count, shouldLoadCanMessagePeer: shouldLoadCanMessagePeer)
return signal
|> map { view, updateType -> ChatListNodeViewUpdate in
return ChatListNodeViewUpdate(list: EngineChatList(view, accountPeerId: accountPeerId), type: updateType, scrollPosition: nil)
}
case let .navigation(index, _):
guard case let .chatList(index) = index else {
return .never()
}
var first = true
return account.viewTracker.aroundChatListView(groupId: groupId._asGroup(), filterPredicate: filterPredicate, index: index, count: 80, shouldLoadCanMessagePeer: shouldLoadCanMessagePeer)
|> map { view, updateType -> ChatListNodeViewUpdate in
let genericType: ViewUpdateType
if first {
first = false
genericType = ViewUpdateType.UpdateVisible
} else {
genericType = updateType
}
return ChatListNodeViewUpdate(list: EngineChatList(view, accountPeerId: accountPeerId), type: genericType, scrollPosition: nil)
}
case let .scroll(index, sourceIndex, scrollPosition, animated, _):
guard case let .chatList(index) = index else {
return .never()
}
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > .chatList(index) ? .Down : .Up
let chatScrollPosition: ChatListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
var first = true
return account.viewTracker.aroundChatListView(groupId: groupId._asGroup(), filterPredicate: filterPredicate, index: index, count: 80, shouldLoadCanMessagePeer: shouldLoadCanMessagePeer)
|> map { view, updateType -> ChatListNodeViewUpdate in
let genericType: ViewUpdateType
let scrollPosition: ChatListNodeViewScrollPosition? = first ? chatScrollPosition : nil
if first {
first = false
genericType = ViewUpdateType.UpdateVisible
} else {
genericType = updateType
}
return ChatListNodeViewUpdate(list: EngineChatList(view, accountPeerId: accountPeerId), type: genericType, scrollPosition: scrollPosition)
}
}
case let .forum(peerId):
let viewKey: PostboxViewKey = .messageHistoryThreadIndex(
id: peerId,
summaryComponents: ChatListEntrySummaryComponents(
components: [
ChatListEntryMessageTagSummaryKey(
tag: .unseenPersonalMessage,
actionType: PendingMessageActionType.consumeUnseenPersonalMessage
): ChatListEntrySummaryComponents.Component(
tagSummary: ChatListEntryMessageTagSummaryComponent(namespace: Namespaces.Message.Cloud),
actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(namespace: Namespaces.Message.Cloud)
),
ChatListEntryMessageTagSummaryKey(
tag: .unseenReaction,
actionType: PendingMessageActionType.readReaction
): ChatListEntrySummaryComponents.Component(
tagSummary: ChatListEntryMessageTagSummaryComponent(namespace: Namespaces.Message.Cloud),
actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(namespace: Namespaces.Message.Cloud)
)
]
)
)
let readStateKey: PostboxViewKey = .combinedReadState(peerId: peerId, handleThreads: false)
var isFirst = false
return account.postbox.combinedView(keys: [viewKey, readStateKey])
|> map { views -> ChatListNodeViewUpdate in
guard let view = views.views[viewKey] as? MessageHistoryThreadIndexView else {
preconditionFailure()
}
guard let readStateView = views.views[readStateKey] as? CombinedReadStateView else {
preconditionFailure()
}
var maxReadId: Int32 = 0
if let state = readStateView.state?.states.first(where: { $0.0 == Namespaces.Message.Cloud }) {
if case let .idBased(maxIncomingReadId, _, _, _, _) = state.1 {
maxReadId = maxIncomingReadId
}
}
var items: [EngineChatList.Item] = []
for item in view.items {
guard let peer = view.peer else {
continue
}
guard let data = item.info.get(MessageHistoryThreadData.self) else {
continue
}
let defaultPeerNotificationSettings: TelegramPeerNotificationSettings = (view.peerNotificationSettings as? TelegramPeerNotificationSettings) ?? .defaultSettings
var hasUnseenMentions = false
var isMuted = false
switch data.notificationSettings.muteState {
case .muted:
isMuted = true
case .unmuted:
isMuted = false
case .default:
if case .default = data.notificationSettings.muteState {
if case .muted = defaultPeerNotificationSettings.muteState {
isMuted = true
}
}
}
if let info = item.tagSummaryInfo[ChatListEntryMessageTagSummaryKey(
tag: .unseenPersonalMessage,
actionType: PendingMessageActionType.consumeUnseenPersonalMessage
)] {
hasUnseenMentions = (info.tagSummaryCount ?? 0) > (info.actionsSummaryCount ?? 0)
}
var hasUnseenReactions = false
if let info = item.tagSummaryInfo[ChatListEntryMessageTagSummaryKey(
tag: .unseenReaction,
actionType: PendingMessageActionType.readReaction
)] {
hasUnseenReactions = (info.tagSummaryCount ?? 0) != 0// > (info.actionsSummaryCount ?? 0)
}
let pinnedIndex: EngineChatList.Item.PinnedIndex
if let index = item.pinnedIndex {
pinnedIndex = .index(index)
} else {
pinnedIndex = .none
}
var topicMaxIncomingReadId = data.maxIncomingReadId
if data.maxIncomingReadId == 0 && maxReadId != 0 && Int64(maxReadId) <= item.id {
topicMaxIncomingReadId = max(topicMaxIncomingReadId, maxReadId)
}
let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: topicMaxIncomingReadId, maxOutgoingReadId: data.maxOutgoingReadId, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))]), isMuted: false)
var draft: EngineChatList.Draft?
if let embeddedState = item.embeddedInterfaceState, let _ = embeddedState.overrideChatTimestamp {
if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) {
if let text = opaqueState.synchronizeableInputState?.text {
draft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? [])
}
}
}
items.append(EngineChatList.Item(
id: .forum(item.id),
index: .forum(pinnedIndex: pinnedIndex, timestamp: item.index.timestamp, threadId: item.id, namespace: item.index.id.namespace, id: item.index.id.id),
messages: item.topMessage.flatMap { [EngineMessage($0)] } ?? [],
readCounters: readCounters,
isMuted: isMuted,
draft: draft,
threadData: data,
renderedPeer: EngineRenderedPeer(peer: EnginePeer(peer)),
presence: nil,
hasUnseenMentions: hasUnseenMentions,
hasUnseenReactions: hasUnseenReactions,
forumTopicData: nil,
topForumTopicItems: [],
hasFailed: false,
isContact: false,
autoremoveTimeout: nil,
storyStats: nil,
displayAsTopicList: false,
isPremiumRequiredToMessage: false,
mediaDraftContentType: nil
))
}
let list = EngineChatList(
items: items.reversed(),
groupItems: [],
additionalItems: [],
hasEarlier: false,
hasLater: false,
isLoading: view.isLoading
)
let type: ViewUpdateType
if isFirst {
type = .Initial
} else {
type = .Generic
}
isFirst = false
return ChatListNodeViewUpdate(list: list, type: type, scrollPosition: nil)
}
case let .savedMessagesChats(peerId):
let viewKey: PostboxViewKey = .savedMessagesIndex(peerId: peerId)
let interfaceStateKey: PostboxViewKey = .chatInterfaceState(peerId: peerId)
var isFirst = true
return account.postbox.combinedView(keys: [viewKey, interfaceStateKey])
|> map { views -> ChatListNodeViewUpdate in
guard let view = views.views[viewKey] as? MessageHistorySavedMessagesIndexView else {
preconditionFailure()
}
var draft: EngineChatList.Draft?
if let interfaceStateView = views.views[interfaceStateKey] as? ChatInterfaceStateView {
if let embeddedState = interfaceStateView.value, let _ = embeddedState.overrideChatTimestamp {
if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) {
if let text = opaqueState.synchronizeableInputState?.text {
draft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? [])
}
}
}
}
var items: [EngineChatList.Item] = []
for item in view.items {
guard let sourcePeer = item.peer else {
continue
}
let sourceId = PeerId(item.id)
var messages: [EngineMessage] = []
if let topMessage = item.topMessage {
messages.append(EngineMessage(topMessage))
}
let mappedMessageIndex = MessageIndex(id: MessageId(peerId: sourceId, namespace: item.index.id.namespace, id: item.index.id.id), timestamp: item.index.timestamp)
let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: Int32(item.unreadCount), markedUnread: item.markedUnread))]), isMuted: false)
var itemDraft: EngineChatList.Draft?
if let embeddedState = item.embeddedInterfaceState, let _ = embeddedState.overrideChatTimestamp {
if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) {
if let text = opaqueState.synchronizeableInputState?.text {
itemDraft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? [])
}
}
}
items.append(EngineChatList.Item(
id: .chatList(sourceId),
index: .chatList(ChatListIndex(pinningIndex: item.pinnedIndex.flatMap(UInt16.init), messageIndex: mappedMessageIndex)),
messages: messages,
readCounters: readCounters,
isMuted: false,
draft: sourceId == accountPeerId ? draft : itemDraft,
threadData: nil,
renderedPeer: EngineRenderedPeer(peer: EnginePeer(sourcePeer)),
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
forumTopicData: nil,
topForumTopicItems: [],
hasFailed: false,
isContact: false,
autoremoveTimeout: nil,
storyStats: nil,
displayAsTopicList: false,
isPremiumRequiredToMessage: false,
mediaDraftContentType: nil
))
}
let list = EngineChatList(
items: items.reversed(),
groupItems: [],
additionalItems: [],
hasEarlier: false,
hasLater: false,
isLoading: view.isLoading
)
let type: ViewUpdateType
if isFirst {
type = .Initial
} else {
type = .Generic
}
isFirst = false
return ChatListNodeViewUpdate(list: list, type: type, scrollPosition: nil)
}
}
}
@@ -0,0 +1,553 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ListSectionHeaderNode
import AppBundle
import ItemListUI
import Markdown
import AccountContext
import MergedAvatarsNode
import TextNodeWithEntities
import TextFormat
import AvatarNode
class ChatListNoticeItem: ListViewItem {
enum Action {
case activate
case hide
case buttonChoice(isPositive: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let notice: ChatListNotice
let action: (Action) -> Void
let selectable: Bool = true
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping (Action) -> Void) {
self.context = context
self.theme = theme
self.strings = strings
self.notice = notice
self.action = action
}
func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.action(.activate)
}
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 = ChatListNoticeItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply()
})
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ChatListNoticeItemNode)
if let nodeValue = node() as? ChatListNoticeItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem == nil)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
private let separatorHeight = 1.0 / UIScreen.main.scale
private let titleFont = Font.semibold(15.0)
private let titleBoldFont = Font.bold(15.0)
private let titleItalicFont = Font.semiboldItalic(15.0)
private let titleBoldItalicFont = Font.semiboldItalic(15.0)
private let textFont = Font.regular(15.0)
private let textBoldFont = Font.semibold(15.0)
private let textItalicFont = Font.italic(15.0)
private let textBoldItalicFont = Font.semiboldItalic(15.0)
private let smallTextFont = Font.regular(14.0)
final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
private let contentContainer: ASDisplayNode
private let titleNode: TextNodeWithEntities
private let textNode: TextNodeWithEntities
private let arrowNode: ASImageNode
private let separatorNode: ASDisplayNode
private var avatarNode: AvatarNode?
private var avatarsNode: MergedAvatarsNode?
private var closeButton: HighlightableButtonNode?
private var okButtonText: TextNode?
private var cancelButtonText: TextNode?
private var okButton: HighlightableButtonNode?
private var cancelButton: HighlightableButtonNode?
private var item: ChatListNoticeItem?
override var apparentHeight: CGFloat {
didSet {
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.bounds.width, height: self.apparentHeight))
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.contentContainer.bounds.height - UIScreenPixel), size: CGSize(width: self.contentContainer.bounds.width, height: UIScreenPixel))
}
}
required init() {
self.contentContainer = ASDisplayNode()
self.titleNode = TextNodeWithEntities()
self.textNode = TextNodeWithEntities()
self.arrowNode = ASImageNode()
self.separatorNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.contentContainer.clipsToBounds = true
self.clipsToBounds = true
self.contentContainer.addSubnode(self.titleNode.textNode)
self.contentContainer.addSubnode(self.textNode.textNode)
self.contentContainer.addSubnode(self.arrowNode)
self.addSubnode(self.contentContainer)
self.addSubnode(self.separatorNode)
self.zPosition = 1.0
}
@objc private func closePressed() {
guard let item = self.item else {
return
}
item.action(.hide)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListNoticeItem, params, nextItem == nil)
apply()
}
func asyncLayout() -> (_ item: ChatListNoticeItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let previousItem = self.item
let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.titleNode)
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let makeOkButtonTextLayout = TextNode.asyncLayout(self.okButtonText)
let makeCancelButtonTextLayout = TextNode.asyncLayout(self.cancelButtonText)
return { item, params, last in
let baseWidth = params.width - params.leftInset - params.rightInset
let _ = baseWidth
let sideInset: CGFloat = params.leftInset + 16.0
let rightInset: CGFloat = sideInset + 24.0
var titleRightInset = rightInset - 4.0
let verticalInset: CGFloat = 9.0
var spacing: CGFloat = 0.0
let themeUpdated = item.theme !== previousItem?.theme
let titleString: NSAttributedString
let textString: NSAttributedString
var avatarPeer: EnginePeer?
var avatarPeers: [EnginePeer] = []
var okButtonLayout: (TextNodeLayout, () -> TextNode)?
var cancelButtonLayout: (TextNodeLayout, () -> TextNode)?
var alignment: NSTextAlignment = .left
switch item.notice {
case let .clearStorage(sizeFraction):
let sizeString = dataSizeString(Int64(sizeFraction), formatting: DataSizeStringFormatting(strings: item.strings, decimalSeparator: "."))
let rawTitleString = item.strings.ChatList_StorageHintTitle(sizeString)
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
if let range = rawTitleString.ranges.first {
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
}
titleString = titleStringValue
textString = NSAttributedString(string: item.strings.ChatList_StorageHintText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case .setupPassword:
titleString = NSAttributedString(string: item.strings.Settings_SuggestSetupPasswordTitle, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
textString = NSAttributedString(string: item.strings.Settings_SuggestSetupPasswordText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case let .premiumUpgrade(discount):
let discountString = "\(discount)%"
let rawTitleString = item.strings.ChatList_PremiumAnnualUpgradeTitle(discountString)
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
if let range = rawTitleString.ranges.first {
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
}
titleString = titleStringValue
textString = NSAttributedString(string: item.strings.ChatList_PremiumAnnualUpgradeText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case let .premiumAnnualDiscount(discount):
let discountString = "\(discount)%"
let rawTitleString = item.strings.ChatList_PremiumAnnualDiscountTitle(discountString)
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
if let range = rawTitleString.ranges.first {
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
}
titleString = titleStringValue
textString = NSAttributedString(string: item.strings.ChatList_PremiumAnnualDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
titleRightInset = sideInset
case let .premiumRestore(discount):
let discountString = "\(discount)%"
let rawTitleString = item.strings.ChatList_PremiumRestoreDiscountTitle(discountString)
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
if let range = rawTitleString.ranges.first {
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
}
titleString = titleStringValue
textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case .xmasPremiumGift:
titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumXmasGiftTitle, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil }))
textString = NSAttributedString(string: item.strings.ChatList_PremiumXmasGiftText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case .premiumGrace:
titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumGraceTitle, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil }))
textString = NSAttributedString(string: item.strings.ChatList_PremiumGraceText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case .setupBirthday:
titleString = NSAttributedString(string: item.strings.ChatList_AddBirthdayTitle, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
textString = NSAttributedString(string: item.strings.ChatList_AddBirthdayText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case let .birthdayPremiumGift(peers, _):
let title: String
let text: String
if peers.count == 1, let peer = peers.first {
var peerName = peer.compactDisplayTitle
if peerName.count > 20 {
peerName = peerName.prefix(20).trimmingCharacters(in: .whitespacesAndNewlines) + "\u{2026}"
}
title = item.strings.ChatList_BirthdaySingleTitle(peerName).string
text = item.strings.ChatList_BirthdaySingleText
} else {
title = item.strings.ChatList_BirthdayMultipleTitle(Int32(peers.count))
text = item.strings.ChatList_BirthdayMultipleText
}
titleString = parseMarkdownIntoAttributedString(title, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil }))
textString = NSAttributedString(string: text, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
avatarPeers = Array(peers.prefix(3))
case let .reviewLogin(newSessionReview, totalCount):
spacing = 2.0
alignment = .center
var rawTitleString = item.strings.ChatList_SessionReview_PanelTitle
if totalCount > 1 {
rawTitleString = "1/\(totalCount) \(rawTitleString)"
}
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
titleString = titleStringValue
textString = NSAttributedString(string: item.strings.ChatList_SessionReview_PanelText(newSessionReview.device, newSessionReview.location).string, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
case let .starsSubscriptionLowBalance(amount, peers):
let title: String
let text: String
let starsValue = item.strings.ChatList_SubscriptionsLowBalance_Stars(Int32(clamping: amount.value))
if let peer = peers.first, peers.count == 1 {
title = item.strings.ChatList_SubscriptionsLowBalance_Single_Title(starsValue, peer.compactDisplayTitle).string
text = item.strings.ChatList_SubscriptionsLowBalance_Single_Text
} else {
title = item.strings.ChatList_SubscriptionsLowBalance_Multiple_Title(starsValue).string
text = item.strings.ChatList_SubscriptionsLowBalance_Multiple_Text
}
let attributedTitle = NSMutableAttributedString(string: "⭐️\(title)", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
if let range = attributedTitle.string.range(of: "⭐️") {
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedTitle.string))
attributedTitle.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedTitle.string))
}
titleString = attributedTitle
textString = NSAttributedString(string: text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case let .setupPhoto(accountPeer):
titleString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Title, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
textString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
avatarPeer = accountPeer
case .accountFreeze:
titleString = NSAttributedString(string: item.strings.ChatList_FrozenAccount_Title, font: titleFont, textColor: item.theme.list.itemDestructiveColor)
textString = NSAttributedString(string: item.strings.ChatList_FrozenAccount_Text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case let .link(_, _, title, subtitle):
titleString = stringWithAppliedEntities(title.string, entities: title.entities, baseColor: item.theme.list.itemPrimaryTextColor, linkColor: item.theme.list.itemAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleBoldFont, italicFont: titleItalicFont, boldItalicFont: titleBoldItalicFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil)
textString = stringWithAppliedEntities(subtitle.string, entities: subtitle.entities, baseColor: item.theme.list.itemPrimaryTextColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFont, blockQuoteFont: textFont, message: nil)
}
var leftInset: CGFloat = sideInset
if !avatarPeers.isEmpty {
let avatarsWidth = 30.0 + CGFloat(avatarPeers.count - 1) * 16.0
leftInset += avatarsWidth + 4.0
} else if let _ = avatarPeer {
let avatarsWidth: CGFloat = 40.0
leftInset += avatarsWidth + 6.0
}
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - titleRightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18))
let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 10, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18))
var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.0.size.height + textLayout.0.size.height)
if let okButtonLayout {
contentSize.height += okButtonLayout.0.size.height + 20.0
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if themeUpdated {
strongSelf.contentContainer.backgroundColor = item.theme.chatList.pinnedItemBackgroundColor
strongSelf.separatorNode.backgroundColor = item.theme.chatList.itemSeparatorColor
strongSelf.arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.theme)
}
let _ = titleLayout.1(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: .white, attemptSynchronous: true))
if case .center = alignment {
strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset), size: titleLayout.0.size)
} else {
strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.0.size)
}
let _ = textLayout.1(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: .white, attemptSynchronous: true))
strongSelf.titleNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 1000000.0, height: 1000000.0))
strongSelf.textNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 1000000.0, height: 1000000.0))
if case .center = alignment {
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: strongSelf.titleNode.textNode.frame.maxY + spacing), size: textLayout.0.size)
} else {
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.textNode.frame.maxY + spacing), size: textLayout.0.size)
}
if !avatarPeers.isEmpty {
let avatarsNode: MergedAvatarsNode
if let current = strongSelf.avatarsNode {
avatarsNode = current
} else {
avatarsNode = MergedAvatarsNode()
avatarsNode.isUserInteractionEnabled = false
strongSelf.addSubnode(avatarsNode)
strongSelf.avatarsNode = avatarsNode
}
let avatarSize = CGSize(width: 30.0, height: 30.0)
avatarsNode.update(context: item.context, peers: avatarPeers.map { $0._asPeer() }, synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 16.0, borderWidth: 2.0 - UIScreenPixel, avatarFontSize: 10.0)
let avatarsSize = CGSize(width: avatarSize.width + 16.0 * CGFloat(avatarPeers.count - 1), height: avatarSize.height)
avatarsNode.updateLayout(size: avatarsSize)
avatarsNode.frame = CGRect(origin: CGPoint(x: sideInset - 6.0, y: floor((layout.size.height - avatarsSize.height) / 2.0)), size: avatarsSize)
} else if let avatarsNode = strongSelf.avatarsNode {
avatarsNode.removeFromSupernode()
strongSelf.avatarsNode = nil
}
if let avatarPeer {
let avatarNode: AvatarNode
if let current = strongSelf.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
avatarNode.isUserInteractionEnabled = false
strongSelf.addSubnode(avatarNode)
strongSelf.avatarNode = avatarNode
avatarNode.setPeer(context: item.context, theme: item.theme, peer: avatarPeer, overrideImage: .cameraIcon)
}
let avatarSize = CGSize(width: 40.0, height: 40.0)
avatarNode.frame = CGRect(origin: CGPoint(x: sideInset - 6.0, y: floor((layout.size.height - avatarSize.height) / 2.0)), size: avatarSize)
} else if let avatarNode = strongSelf.avatarNode {
avatarNode.removeFromSupernode()
strongSelf.avatarNode = nil
}
if let image = strongSelf.arrowNode.image {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size)
}
let hasCloseButton: Bool
switch item.notice {
case .xmasPremiumGift, .setupBirthday, .birthdayPremiumGift, .premiumGrace, .starsSubscriptionLowBalance, .setupPhoto, .link:
hasCloseButton = true
default:
hasCloseButton = false
}
if let okButtonLayout, let cancelButtonLayout {
strongSelf.arrowNode.isHidden = true
strongSelf.closeButton?.isHidden = true
let okButton: HighlightableButtonNode
if let current = strongSelf.okButton {
okButton = current
} else {
okButton = HighlightableButtonNode()
strongSelf.okButton = okButton
strongSelf.contentContainer.addSubnode(okButton)
okButton.addTarget(strongSelf, action: #selector(strongSelf.okButtonPressed), forControlEvents: .touchUpInside)
}
let cancelButton: HighlightableButtonNode
if let current = strongSelf.cancelButton {
cancelButton = current
} else {
cancelButton = HighlightableButtonNode()
strongSelf.cancelButton = cancelButton
strongSelf.contentContainer.addSubnode(cancelButton)
cancelButton.addTarget(strongSelf, action: #selector(strongSelf.cancelButtonPressed), forControlEvents: .touchUpInside)
}
let okButtonText = okButtonLayout.1()
if okButtonText !== strongSelf.okButtonText {
strongSelf.okButtonText?.removeFromSupernode()
strongSelf.okButtonText = okButtonText
okButton.addSubnode(okButtonText)
}
let cancelButtonText = cancelButtonLayout.1()
if cancelButtonText !== strongSelf.okButtonText {
strongSelf.cancelButtonText?.removeFromSupernode()
strongSelf.cancelButtonText = cancelButtonText
cancelButton.addSubnode(cancelButtonText)
}
let buttonsWidth: CGFloat = max(min(300.0, params.width), okButtonLayout.0.size.width + cancelButtonLayout.0.size.width + 32.0)
let buttonWidth: CGFloat = floor(buttonsWidth * 0.5)
let buttonHeight: CGFloat = 32.0
let okButtonFrame = CGRect(origin: CGPoint(x: floor((params.width - buttonsWidth) * 0.5), y: strongSelf.textNode.textNode.frame.maxY + 6.0), size: CGSize(width: buttonWidth, height: buttonHeight))
let cancelButtonFrame = CGRect(origin: CGPoint(x: okButtonFrame.maxX, y: strongSelf.textNode.textNode.frame.maxY + 6.0), size: CGSize(width: buttonWidth, height: buttonHeight))
okButton.frame = okButtonFrame
cancelButton.frame = cancelButtonFrame
okButtonText.frame = CGRect(origin: CGPoint(x: floor((okButtonFrame.width - okButtonLayout.0.size.width) * 0.5), y: floor((okButtonFrame.height - okButtonLayout.0.size.height) * 0.5)), size: okButtonLayout.0.size)
cancelButtonText.frame = CGRect(origin: CGPoint(x: floor((cancelButtonFrame.width - cancelButtonLayout.0.size.width) * 0.5), y: floor((cancelButtonFrame.height - cancelButtonLayout.0.size.height) * 0.5)), size: cancelButtonLayout.0.size)
} else {
strongSelf.arrowNode.isHidden = hasCloseButton
if let okButton = strongSelf.okButton {
strongSelf.okButton = nil
okButton.removeFromSupernode()
}
if let cancelButton = strongSelf.cancelButton {
strongSelf.cancelButton = nil
cancelButton.removeFromSupernode()
}
if let okButtonText = strongSelf.okButtonText {
strongSelf.okButtonText = nil
okButtonText.removeFromSupernode()
}
if let cancelButtonText = strongSelf.cancelButtonText {
strongSelf.cancelButtonText = nil
cancelButtonText.removeFromSupernode()
}
if hasCloseButton {
let closeButton: HighlightableButtonNode
if let current = strongSelf.closeButton {
closeButton = current
} else {
closeButton = HighlightableButtonNode()
closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
closeButton.addTarget(self, action: #selector(strongSelf.closePressed), forControlEvents: [.touchUpInside])
strongSelf.contentContainer.addSubnode(closeButton)
strongSelf.closeButton = closeButton
}
if themeUpdated || closeButton.image(for: .normal) == nil {
closeButton.setImage(PresentationResourcesItemList.itemListCloseIconImage(item.theme), for: .normal)
}
let closeButtonSize = closeButton.measure(CGSize(width: 100.0, height: 100.0))
closeButton.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - closeButtonSize.width, y: floor((layout.size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize)
} else {
strongSelf.closeButton?.removeFromSupernode()
strongSelf.closeButton = nil
}
}
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
//strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
switch item.notice {
default:
strongSelf.setRevealOptions((left: [], right: []))
}
}
})
}
}
override public func selected() {
super.selected()
if case .setupPhoto = self.item?.notice {
self.avatarNode?.playCameraAnimation()
}
}
@objc private func okButtonPressed() {
self.item?.action(.buttonChoice(isPositive: true))
}
@objc private func cancelButtonPressed() {
self.item?.action(.buttonChoice(isPositive: false))
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
super.animateInsertion(currentTimestamp, duration: duration, options: options)
//self.transitionOffset = self.bounds.size.height
//self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
}
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
transition.updateSublayerTransformOffset(layer: self.contentContainer.layer, offset: CGPoint(x: offset, y: 0.0))
}
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
if let item = self.item {
item.action(.hide)
}
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
}
}
@@ -0,0 +1,24 @@
import Foundation
import UIKit
import TelegramPresentationData
import TelegramUIPreferences
public final class ChatListPresentationData {
public let theme: PresentationTheme
public let fontSize: PresentationFontSize
public let strings: PresentationStrings
public let dateTimeFormat: PresentationDateTimeFormat
public let nameSortOrder: PresentationPersonNameOrder
public let nameDisplayOrder: PresentationPersonNameOrder
public let disableAnimations: Bool
public init(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) {
self.theme = theme
self.fontSize = fontSize
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.nameSortOrder = nameSortOrder
self.nameDisplayOrder = nameDisplayOrder
self.disableAnimations = disableAnimations
}
}
@@ -0,0 +1,480 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import LegacyComponents
import RadialStatusNode
enum ChatListStatusNodeState: Equatable {
case none
case clock(UIImage?, UIImage?)
case delivered(UIColor)
case read(UIColor)
case progress(UIColor, CGFloat)
case failed(UIColor, UIColor)
func contentNode() -> ChatListStatusContentNode? {
switch self {
case .none:
return nil
case let .clock(frameImage, minImage):
return ChatListStatusClockNode(frameImage: frameImage, minImage: minImage)
case let .delivered(color):
return ChatListStatusChecksNode(color: color)
case let .read(color):
return ChatListStatusChecksNode(color: color)
case let .progress(color, progress):
return ChatListStatusProgressNode(color: color, progress: progress)
case let .failed(fill, foreground):
return ChatListStatusFailedNode(fill: fill, foreground: foreground)
}
}
}
private let transitionDuration = 0.2
class ChatListStatusContentNode: ASDisplayNode {
var fontSize: CGFloat = 17.0
override init() {
super.init()
self.isOpaque = false
}
func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
}
func animateOut(to: ChatListStatusNodeState, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration, removeOnCompletion: false, completion: { _ in
completion()
})
}
func animateIn(from: ChatListStatusNodeState) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: transitionDuration)
}
}
final class ChatListStatusNode: ASDisplayNode {
private(set) var state: ChatListStatusNodeState = .none
var fontSize: CGFloat = 17.0 {
didSet {
self.contentNode?.fontSize = self.fontSize
self.nextContentNode?.fontSize = self.fontSize
}
}
private var contentNode: ChatListStatusContentNode?
private var nextContentNode: ChatListStatusContentNode?
public func transitionToState(_ state: ChatListStatusNodeState, animated: Bool = false, completion: @escaping () -> Void = {}) -> Bool {
if self.state != state {
let currentState = self.state
self.state = state
let contentNode = state.contentNode()
contentNode?.fontSize = self.fontSize
if contentNode?.classForCoder != self.contentNode?.classForCoder {
contentNode?.updateWithState(state, animated: animated)
self.transitionToContentNode(contentNode, state: state, fromState: currentState, animated: animated, completion: completion)
} else {
self.contentNode?.updateWithState(state, animated: animated)
}
return true
} else {
completion()
return false
}
}
private func transitionToContentNode(_ node: ChatListStatusContentNode?, state: ChatListStatusNodeState, fromState: ChatListStatusNodeState, animated: Bool, completion: @escaping () -> Void) {
if let previousContentNode = self.contentNode {
if !animated {
previousContentNode.removeFromSupernode()
self.contentNode = node
if let contentNode = self.contentNode {
self.addSubnode(contentNode)
}
} else {
self.contentNode = node
if let contentNode = self.contentNode {
self.addSubnode(contentNode)
contentNode.frame = self.bounds
if self.isNodeLoaded {
contentNode.animateIn(from: fromState)
contentNode.layout()
}
}
previousContentNode.animateOut(to: state) {
previousContentNode.removeFromSupernode()
}
}
} else {
self.contentNode = node
if let contentNode = self.contentNode {
contentNode.frame = self.bounds
self.addSubnode(contentNode)
if self.isNodeLoaded {
contentNode.layout()
}
}
}
}
override public func layout() {
if let contentNode = self.contentNode {
contentNode.frame = self.bounds
}
}
}
class ChatListStatusClockNode: ChatListStatusContentNode {
private var clockFrameNode: ASImageNode
private var clockMinNode: ASImageNode
init(frameImage: UIImage?, minImage: UIImage?) {
self.clockFrameNode = ASImageNode()
self.clockMinNode = ASImageNode()
super.init()
self.clockFrameNode.image = frameImage
self.clockMinNode.image = minImage
self.addSubnode(self.clockFrameNode)
self.addSubnode(self.clockMinNode)
}
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
if case let .clock(frameImage, minImage) = state {
self.clockFrameNode.image = frameImage
self.clockMinNode.image = minImage
}
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
maybeAddRotationAnimation(self.clockFrameNode.layer, duration: 6.0)
maybeAddRotationAnimation(self.clockMinNode.layer, duration: 1.0)
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.clockFrameNode.layer.removeAllAnimations()
self.clockMinNode.layer.removeAllAnimations()
}
override func layout() {
super.layout()
let bounds = self.bounds
if let frameImage = self.clockFrameNode.image {
self.clockFrameNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - frameImage.size.width) / 2.0), y: floorToScreenPixels((bounds.height - frameImage.size.height) / 2.0)), size: frameImage.size)
}
if let minImage = self.clockMinNode.image {
self.clockMinNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - minImage.size.width) / 2.0), y: floorToScreenPixels((bounds.height - minImage.size.height) / 2.0)), size: minImage.size)
}
}
}
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
if let _ = layer.animation(forKey: "clockFrameAnimation") {
return
}
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
basicAnimation.duration = duration
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
basicAnimation.beginTime = 1.0
layer.add(basicAnimation, forKey: "clockFrameAnimation")
}
private final class StatusChecksNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
let fontSize: CGFloat
init(color: UIColor, progress: CGFloat, fontSize: CGFloat) {
self.color = color
self.progress = progress
self.fontSize = fontSize
super.init()
}
}
private class ChatListStatusChecksNode: ChatListStatusContentNode {
private var state: ChatListStatusNodeState?
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 1.0 {
didSet {
self.setNeedsDisplay()
}
}
override var fontSize: CGFloat {
didSet {
self.setNeedsDisplay()
}
}
init(color: UIColor) {
self.color = color
super.init()
}
func animateProgress(from: CGFloat, to: CGFloat) {
self.pop_removeAllAnimations()
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! ChatListStatusChecksNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! ChatListStatusChecksNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = from as NSNumber
animation.toValue = to as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.2
self.pop_add(animation, forKey: "progress")
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return StatusChecksNodeParameters(color: self.color, progress: self.effectiveProgress, fontSize: self.fontSize)
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? StatusChecksNodeParameters else {
return
}
let scaleFactor = min(1.4, parameters.fontSize / 17.0)
context.translateBy(x: bounds.width / 2.0, y: bounds.height / 2.0)
context.scaleBy(x: scaleFactor, y: scaleFactor)
context.translateBy(x: -bounds.width / 2.0, y: -bounds.height / 2.0)
let progress = parameters.progress
context.setStrokeColor(parameters.color.cgColor)
context.setLineWidth(1.0 + UIScreenPixel)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
context.saveGState()
var s1 = CGPoint(x: 9.0, y: 13.0)
var s2 = CGPoint(x: 5.0, y: 13.0)
let p1 = CGPoint(x: 3.5, y: 3.5)
let p2 = CGPoint(x: 7.5 - UIScreenPixel, y: -8.0)
let check1FirstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
let check2FirstSegment: CGFloat = max(0.0, min(1.0, (progress - 1.0) * 3.0))
let firstProgress = max(0.0, min(1.0, progress))
let secondProgress = max(0.0, min(1.0, progress - 1.0))
let scale: CGFloat = 1.2
context.translateBy(x: 16.0, y: 13.0)
context.scaleBy(x: scale - abs((scale - 1.0) * (firstProgress - 0.5) / 0.5), y: scale - abs((scale - 1.0) * (firstProgress - 0.5) / 0.5))
s1 = s1.offsetBy(dx: -16.0, dy: -13.0)
if !check1FirstSegment.isZero {
if check1FirstSegment < 1.0 {
context.move(to: CGPoint(x: s1.x + p1.x * check1FirstSegment, y: s1.y + p1.y * check1FirstSegment))
context.addLine(to: s1)
} else {
let secondSegment = (min(1.0, progress) - 0.33) * 1.5
context.move(to: CGPoint(x: s1.x + p1.x + p2.x * secondSegment, y: s1.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s1.x + p1.x, y: s1.y + p1.y))
context.addLine(to: CGPoint(x: s1.x + p1.x * min(1.0, check2FirstSegment), y: s1.y + p1.y * min(1.0, check2FirstSegment)))
}
}
context.strokePath()
context.restoreGState()
context.translateBy(x: 12.0, y: 13.0)
context.scaleBy(x: scale - abs((scale - 1.0) * (secondProgress - 0.5) / 0.5), y: scale - abs((scale - 1.0) * (secondProgress - 0.5) / 0.5))
s2 = s2.offsetBy(dx: -12.0, dy: -13.0)
if !check2FirstSegment.isZero {
if check2FirstSegment < 1.0 {
context.move(to: CGPoint(x: s2.x + p1.x * check2FirstSegment, y: s2.y + p1.y * check2FirstSegment))
context.addLine(to: s2)
} else {
let secondSegment = (max(0.0, (progress - 1.0)) - 0.33) * 1.5
context.move(to: CGPoint(x: s2.x + p1.x + p2.x * secondSegment, y: s2.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s2.x + p1.x, y: s2.y + p1.y))
context.addLine(to: s2)
}
}
context.strokePath()
}
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
switch state {
case let .delivered(color), let .read(color):
self.color = color
default:
break
}
var animating = false
if let previousState = self.state, case .delivered = previousState, case .read = state, animated {
animating = true
self.animateProgress(from: 1.0, to: 2.0)
}
if !animating {
if case .delivered = state {
self.effectiveProgress = 1.0
} else if case .read = state {
self.effectiveProgress = 2.0
}
}
self.state = state
}
override func animateIn(from: ChatListStatusNodeState) {
if let state = self.state, case .delivered = state {
self.animateProgress(from: 0.0, to: 1.0)
} else {
super.animateIn(from: from)
}
}
}
private final class ChatListStatusFailedNodeParameters: NSObject {
let fill: UIColor
let foreground: UIColor
init(fill: UIColor, foreground: UIColor) {
self.fill = fill
self.foreground = foreground
super.init()
}
}
private class ChatListStatusFailedNode: ChatListStatusContentNode {
private var state: ChatListStatusNodeState?
var fill: UIColor {
didSet {
self.setNeedsDisplay()
}
}
var foreground: UIColor {
didSet {
self.setNeedsDisplay()
}
}
init(fill: UIColor, foreground: UIColor) {
self.fill = fill
self.foreground = foreground
super.init()
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return ChatListStatusFailedNodeParameters(fill: self.fill, foreground: self.foreground)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? ChatListStatusFailedNodeParameters else {
return
}
let diameter: CGFloat = 14.0
let rect = CGRect(origin: CGPoint(x: floor((bounds.width - diameter) / 2.0), y: floor((bounds.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)).offsetBy(dx: 1.0, dy: UIScreenPixel)
context.setFillColor(parameters.fill.cgColor)
context.fillEllipse(in: rect)
context.setStrokeColor(parameters.foreground.cgColor)
let string = NSAttributedString(string: "!", font: Font.medium(12.0), textColor: parameters.foreground)
let stringRect = string.boundingRect(with: rect.size, options: .usesLineFragmentOrigin, context: nil)
UIGraphicsPushContext(context)
string.draw(at: CGPoint(x: rect.minX + floor((rect.width - stringRect.width) / 2.0), y: 1.0 - UIScreenPixel + rect.minY + floor((rect.height - stringRect.height) / 2.0)))
UIGraphicsPopContext()
}
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
switch state {
case let .failed(fill, foreground):
self.fill = fill
self.foreground = foreground
default:
break
}
self.state = state
}
}
private class ChatListStatusProgressNode: ChatListStatusContentNode {
private let statusNode: RadialStatusNode
init(color: UIColor, progress: CGFloat) {
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
super.init()
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false, animateRotation: true))
self.addSubnode(self.statusNode)
}
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
if case let .progress(color, progress) = state {
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false, animateRotation: true), animated: animated, completion: {})
}
}
override func layout() {
super.layout()
let bounds = self.bounds
let size = CGSize(width: 12.0, height: 12.0)
self.statusNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - size.width) / 2.0), y: floorToScreenPixels((bounds.height - size.height) / 2.0)), size: size)
}
}
@@ -0,0 +1,166 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import Display
import SwiftSignalKit
import TelegramPresentationData
import ChatTitleActivityNode
import LocalizedPeerData
final class ChatListInputActivitiesNode: ASDisplayNode {
private let activityNode: ChatTitleActivityNode
override init() {
self.activityNode = ChatTitleActivityNode()
super.init()
self.addSubnode(self.activityNode)
}
func asyncLayout() -> (CGSize, ChatListPresentationData, UIColor, EnginePeer.Id?, [(EnginePeer, PeerInputActivity)]) -> (CGSize, () -> Void) {
return { [weak self] boundingSize, presentationData, color, peerId, activities in
let strings = presentationData.strings
let textFont = Font.regular(floor(presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
var state = ChatTitleActivityNodeState.none
if !activities.isEmpty {
var commonKey: Int32? = activities[0].1.key
for i in 1 ..< activities.count {
if activities[i].1.key != commonKey {
commonKey = nil
break
}
}
let lightColor = color.withAlphaComponent(0.85)
if activities.count == 1 {
if activities[0].0.id == peerId {
let text: String
switch activities[0].1 {
case .uploadingVideo:
text = strings.Activity_UploadingVideo
case .uploadingInstantVideo:
text = strings.Activity_UploadingVideoMessage
case .uploadingPhoto:
text = strings.Activity_UploadingPhoto
case .uploadingFile:
text = strings.Activity_UploadingDocument
case .recordingVoice:
text = strings.Activity_RecordingAudio
case .recordingInstantVideo:
text = strings.Activity_RecordingVideoMessage
case .playingGame:
text = strings.Activity_PlayingGame
case .typingText:
text = strings.DialogList_Typing
case .choosingSticker:
text = strings.Activity_ChoosingSticker
case let .interactingWithEmoji(emoticon, _, _):
text = strings.Activity_TappingInteractiveEmoji(emoticon).string
case .speakingInGroupCall, .seeingEmojiInteraction:
text = ""
}
let string = NSAttributedString(string: text, font: textFont, textColor: color)
switch activities[0].1 {
case .typingText:
state = .typingText(string, lightColor)
case .recordingVoice:
state = .recordingVoice(string, lightColor)
case .recordingInstantVideo:
state = .recordingVideo(string, lightColor)
case .uploadingFile, .uploadingInstantVideo, .uploadingPhoto, .uploadingVideo:
state = .uploading(string, lightColor)
case .playingGame:
state = .playingGame(string, lightColor)
case .speakingInGroupCall:
state = .typingText(string, lightColor)
case .choosingSticker:
state = .choosingSticker(string, lightColor)
case .interactingWithEmoji:
state = .interactingWithEmoji(string, lightColor)
case .seeingEmojiInteraction:
state = .none
}
} else {
let text: String
if let _ = commonKey {
let peerTitle = activities[0].0.compactDisplayTitle
switch activities[0].1 {
case .uploadingVideo:
text = strings.DialogList_SingleUploadingVideoSuffix(peerTitle).string
case .uploadingInstantVideo:
text = strings.DialogList_SingleUploadingVideoSuffix(peerTitle).string
case .uploadingPhoto:
text = strings.DialogList_SingleUploadingPhotoSuffix(peerTitle).string
case .uploadingFile:
text = strings.DialogList_SingleUploadingFileSuffix(peerTitle).string
case .recordingVoice:
text = strings.DialogList_SingleRecordingAudioSuffix(peerTitle).string
case .recordingInstantVideo:
text = strings.DialogList_SingleRecordingVideoMessageSuffix(peerTitle).string
case .playingGame:
text = strings.DialogList_SinglePlayingGameSuffix(peerTitle).string
case .typingText:
text = strings.DialogList_SingleTypingSuffix(peerTitle).string
case .choosingSticker:
text = strings.DialogList_SingleChoosingStickerSuffix(peerTitle).string
case .speakingInGroupCall, .seeingEmojiInteraction, .interactingWithEmoji:
text = ""
}
} else {
text = activities[0].0.compactDisplayTitle
}
let string = NSAttributedString(string: text, font: textFont, textColor: color)
switch activities[0].1 {
case .typingText:
state = .typingText(string, lightColor)
case .recordingVoice:
state = .recordingVoice(string, lightColor)
case .recordingInstantVideo:
state = .recordingVideo(string, lightColor)
case .uploadingFile, .uploadingInstantVideo, .uploadingPhoto, .uploadingVideo:
state = .uploading(string, lightColor)
case .playingGame:
state = .playingGame(string, lightColor)
case .speakingInGroupCall:
state = .typingText(string, lightColor)
case .choosingSticker:
state = .choosingSticker(string, lightColor)
case .seeingEmojiInteraction, .interactingWithEmoji:
state = .none
}
}
} else {
let string: NSAttributedString
if activities.count > 1 {
let peerTitle = activities[0].0.compactDisplayTitle
if activities.count == 2 {
let secondPeerTitle = activities[1].0.compactDisplayTitle
string = NSAttributedString(string: strings.DialogList_MultipleTypingPair(peerTitle, secondPeerTitle).string, font: textFont, textColor: color)
} else {
string = NSAttributedString(string: strings.DialogList_MultipleTyping(peerTitle, strings.DialogList_MultipleTypingSuffix(activities.count - 1).string).string, font: textFont, textColor: color)
}
} else {
string = NSAttributedString(string: strings.DialogList_MultipleTypingSuffix(activities.count).string, font: textFont, textColor: color)
}
state = .typingText(string, lightColor)
}
}
return (boundingSize, {
if let strongSelf = self {
let _ = strongSelf.activityNode.transitionToState(state, animation: .none)
let size = strongSelf.activityNode.updateLayout(CGSize(width: boundingSize.width - 12.0, height: boundingSize.height), alignment: .left)
strongSelf.activityNode.frame = CGRect(origin: CGPoint(x: -3.0, y: 1.0), size: size)
}
})
}
}
}
@@ -0,0 +1,222 @@
import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
import MergeLists
import SearchUI
import TelegramUIPreferences
struct ChatListNodeView {
let originalList: EngineChatList
let filteredEntries: [ChatListNodeEntry]
let isLoading: Bool
let filter: ChatListFilter?
}
enum ChatListNodeViewTransitionReason {
case initial
case interactiveChanges
case holeChanges
case reload
}
struct ChatListNodeViewTransitionInsertEntry {
let index: Int
let previousIndex: Int?
let entry: ChatListNodeEntry
let directionHint: ListViewItemOperationDirectionHint?
}
struct ChatListNodeViewTransitionUpdateEntry {
let index: Int
let previousIndex: Int
let entry: ChatListNodeEntry
let directionHint: ListViewItemOperationDirectionHint?
}
struct ChatListNodeViewTransition {
let chatListView: ChatListNodeView
let deleteItems: [ListViewDeleteItem]
let insertEntries: [ChatListNodeViewTransitionInsertEntry]
let updateEntries: [ChatListNodeViewTransitionUpdateEntry]
let options: ListViewDeleteAndInsertOptions
let scrollToItem: ListViewScrollToItem?
let stationaryItemRange: (Int, Int)?
let adjustScrollToFirstItem: Bool
let animateCrossfade: Bool
}
public enum ChatListNodeViewScrollPosition {
case index(index: ChatListIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
}
func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toView: ChatListNodeView, reason: ChatListNodeViewTransitionReason, previewing: Bool, disableAnimations: Bool, account: Account, scrollPosition: ChatListNodeViewScrollPosition?, searchMode: Bool, forceAllUpdated: Bool) -> Signal<ChatListNodeViewTransition, NoError> {
return Signal<ChatListNodeViewTransition, NoError> { subscriber in
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries, allUpdated: forceAllUpdated)
var adjustedDeleteIndices: [ListViewDeleteItem] = []
let previousCount: Int
if let fromView = fromView {
previousCount = fromView.filteredEntries.count
} else {
previousCount = 0
}
for index in deleteIndices {
adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil))
}
var adjustedIndicesAndItems: [ChatListNodeViewTransitionInsertEntry] = []
var adjustedUpdateItems: [ChatListNodeViewTransitionUpdateEntry] = []
let updatedCount = toView.filteredEntries.count
var options: ListViewDeleteAndInsertOptions = []
var maxAnimatedInsertionIndex = -1
var scrollToItem: ListViewScrollToItem?
switch reason {
case .initial:
let _ = options.insert(.LowLatency)
let _ = options.insert(.Synchronous)
case .interactiveChanges:
for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) {
let adjustedIndex = updatedCount - 1 - index
if adjustedIndex == maxAnimatedInsertionIndex + 1 {
maxAnimatedInsertionIndex += 1
}
}
var minTimestamp: Int32?
var maxTimestamp: Int32?
for (_, item, _) in indicesAndItems {
if case .PeerEntry = item, case let .index(index) = item.sortIndex, case let .chatList(chatListIndex) = index, chatListIndex.pinningIndex == nil {
let timestamp = chatListIndex.messageIndex.timestamp
if minTimestamp == nil || timestamp < minTimestamp! {
minTimestamp = timestamp
}
if maxTimestamp == nil || timestamp > maxTimestamp! {
maxTimestamp = timestamp
}
}
}
let _ = options.insert(.AnimateAlpha)
if !disableAnimations {
let _ = options.insert(.AnimateInsertion)
}
case .reload:
break
case .holeChanges:
break
}
for (index, entry, previousIndex) in indicesAndItems {
let adjustedIndex = updatedCount - 1 - index
let adjustedPrevousIndex: Int?
if let previousIndex = previousIndex {
adjustedPrevousIndex = previousCount - 1 - previousIndex
} else {
adjustedPrevousIndex = nil
}
var directionHint: ListViewItemOperationDirectionHint?
if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex {
directionHint = .Down
}
adjustedIndicesAndItems.append(ChatListNodeViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint))
}
for (index, entry, previousIndex) in updateIndices {
let adjustedIndex = updatedCount - 1 - index
let adjustedPreviousIndex = previousCount - 1 - previousIndex
let directionHint: ListViewItemOperationDirectionHint? = nil
adjustedUpdateItems.append(ChatListNodeViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint))
}
if let scrollPosition = scrollPosition {
switch scrollPosition {
case let .index(scrollIndex, position, directionHint, animated):
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if entry.sortIndex >= .index(.chatList(scrollIndex)) {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint)
break
}
index -= 1
}
if scrollToItem == nil {
var index = 0
for entry in toView.filteredEntries.reversed() {
if entry.sortIndex < .index(.chatList(scrollIndex)) {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint)
break
}
index += 1
}
}
}
}
var fromEmptyView: Bool
fromEmptyView = false
var animateCrossfade: Bool
animateCrossfade = false
if let fromView = fromView {
var wasSingleHeader = false
if fromView.filteredEntries.count == 1, case .HeaderEntry = fromView.filteredEntries[0] {
wasSingleHeader = true
}
var isSingleHeader = false
if toView.filteredEntries.count == 1, case .HeaderEntry = toView.filteredEntries[0] {
isSingleHeader = true
}
if (wasSingleHeader || isSingleHeader), case .interactiveChanges = reason {
if wasSingleHeader != isSingleHeader {
if wasSingleHeader {
animateCrossfade = true
options.remove(.AnimateInsertion)
options.remove(.AnimateAlpha)
} else {
let _ = options.insert(.AnimateInsertion)
}
}
} else if fromView.filteredEntries.isEmpty || fromView.filter != toView.filter {
var updateEmpty = true
if !fromView.filteredEntries.isEmpty, let fromFilter = fromView.filter, let toFilter = toView.filter, case var .filter(_, _, _, fromData) = fromFilter, case let .filter(_, _, _, toData) = toFilter, fromData.includePeers.pinnedPeers != toData.includePeers.pinnedPeers {
fromData.includePeers = toData.includePeers
if fromData == toData {
options.insert(.AnimateInsertion)
updateEmpty = false
}
}
if updateEmpty {
options.remove(.AnimateInsertion)
options.remove(.AnimateAlpha)
fromEmptyView = true
}
}
} else {
fromEmptyView = true
}
if let fromView = fromView, !fromView.isLoading, toView.isLoading {
options.remove(.AnimateInsertion)
options.remove(.AnimateAlpha)
}
var adjustScrollToFirstItem = false
if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 1 {
adjustScrollToFirstItem = true
}
subscriber.putNext(ChatListNodeViewTransition(chatListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: nil, adjustScrollToFirstItem: adjustScrollToFirstItem, animateCrossfade: animateCrossfade))
subscriber.putCompletion()
return EmptyDisposable
}
}
@@ -0,0 +1,245 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import AsyncDisplayKit
import TelegramPresentationData
import AccountContext
import Postbox
import TelegramUIPreferences
import TelegramCore
public func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilter, Int, Bool)]), NoError> {
return context.engine.peers.updatedChatListFilters()
|> distinctUntilChanged
|> mapToSignal { filters -> Signal<(Int, [(ChatListFilter, Int, Bool)]), NoError> in
var unreadCountItems: [UnreadMessageCountsItem] = []
unreadCountItems.append(.totalInGroup(.root))
var additionalPeerIds = Set<PeerId>()
var additionalGroupIds = Set<PeerGroupId>()
for case let .filter(_, _, _, data) in filters {
additionalPeerIds.formUnion(data.includePeers.peers)
additionalPeerIds.formUnion(data.excludePeers)
if !data.excludeArchived {
additionalGroupIds.insert(Namespaces.PeerGroup.archive)
}
}
if !additionalPeerIds.isEmpty {
for peerId in additionalPeerIds {
unreadCountItems.append(.peer(id: peerId, handleThreads: true))
}
}
for groupId in additionalGroupIds {
unreadCountItems.append(.totalInGroup(groupId))
}
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications]))
let unreadKey: PostboxViewKey = .unreadCounts(items: unreadCountItems)
var keys: [PostboxViewKey] = []
keys.append(globalNotificationsKey)
keys.append(unreadKey)
for peerId in additionalPeerIds {
keys.append(.basicPeer(peerId))
}
return context.account.postbox.combinedView(keys: keys)
|> map { view -> (Int, [(ChatListFilter, Int, Bool)]) in
guard let unreadCounts = view.views[unreadKey] as? UnreadMessageCountsView else {
return (0, [])
}
var globalNotificationSettings: GlobalNotificationSettingsSet
if let settingsView = view.views[globalNotificationsKey] as? PreferencesView, let settings = settingsView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
globalNotificationSettings = settings.effective
} else {
globalNotificationSettings = GlobalNotificationSettings.defaultSettings.effective
}
var result: [(ChatListFilter, Int, Bool)] = []
var peerTagAndCount: [PeerId: (PeerSummaryCounterTags, Int, Bool, PeerGroupId?, Bool)] = [:]
var totalStates: [PeerGroupId: ChatListTotalUnreadState] = [:]
for entry in unreadCounts.entries {
switch entry {
case let .total(_, state):
totalStates[.root] = state
case let .totalInGroup(groupId, state):
totalStates[groupId] = state
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, peerView.isContact)
var peerCount = Int(state.count)
if state.isUnread {
peerCount = max(1, peerCount)
}
var isMuted = false
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
if case .muted = notificationSettings.muteState {
isMuted = true
} else if case .default = notificationSettings.muteState {
if let peer = peerView.peer {
if peer is TelegramUser {
isMuted = !globalNotificationSettings.privateChats.enabled
} else if peer is TelegramGroup {
isMuted = !globalNotificationSettings.groupChats.enabled
} else if let channel = peer as? TelegramChannel {
switch channel.info {
case .group:
isMuted = !globalNotificationSettings.groupChats.enabled
case .broadcast:
isMuted = !globalNotificationSettings.channels.enabled
}
}
}
}
}
if isMuted {
peerTagAndCount[peerId] = (tag, peerCount, false, peerView.groupId, true)
} else {
peerTagAndCount[peerId] = (tag, peerCount, true, peerView.groupId, false)
}
}
}
}
}
let totalBadge = 0
for filter in filters {
var count = 0
var unmutedUnreadCount = 0
if case let .filter(_, _, _, data) = filter {
var tags: [PeerSummaryCounterTags] = []
if data.categories.contains(.contacts) {
tags.append(.contact)
}
if data.categories.contains(.nonContacts) {
tags.append(.nonContact)
}
if data.categories.contains(.groups) {
tags.append(.group)
}
if data.categories.contains(.bots) {
tags.append(.bot)
}
if data.categories.contains(.channels) {
tags.append(.channel)
}
if let totalState = totalStates[.root] {
for tag in tags {
if data.excludeMuted {
if let value = totalState.filteredCounters[tag] {
if value.chatCount != 0 {
count += Int(value.chatCount)
unmutedUnreadCount += Int(value.chatCount)
}
}
} else {
if let value = totalState.absoluteCounters[tag] {
count += Int(value.chatCount)
}
if let value = totalState.filteredCounters[tag] {
if value.chatCount != 0 {
unmutedUnreadCount += Int(value.chatCount)
}
}
}
}
}
if !data.excludeArchived {
if let totalState = totalStates[Namespaces.PeerGroup.archive] {
for tag in tags {
if data.excludeMuted {
if let value = totalState.filteredCounters[tag] {
if value.chatCount != 0 {
count += Int(value.chatCount)
unmutedUnreadCount += Int(value.chatCount)
}
}
} else {
if let value = totalState.absoluteCounters[tag] {
count += Int(value.chatCount)
}
if let value = totalState.filteredCounters[tag] {
if value.chatCount != 0 {
unmutedUnreadCount += Int(value.chatCount)
}
}
}
}
}
}
for peerId in data.includePeers.peers {
if let (tag, peerCount, hasUnmuted, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue {
var matches = true
if tags.contains(tag) {
if isMuted && data.excludeMuted {
} else {
matches = false
}
}
if matches {
let matchesGroup: Bool
switch groupId {
case .root:
matchesGroup = true
case .group:
if groupId == Namespaces.PeerGroup.archive {
matchesGroup = !data.excludeArchived
} else {
matchesGroup = false
}
}
if matchesGroup && peerCount != 0 {
count += 1
if hasUnmuted {
unmutedUnreadCount += 1
}
}
}
}
}
for peerId in data.excludePeers {
if let (tag, peerCount, _, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue {
var matches = false
if tags.contains(tag) {
matches = true
if isMuted && data.excludeMuted {
matches = false
}
}
if matches {
let matchesGroup: Bool
switch groupId {
case .root:
matchesGroup = true
case .group:
if groupId == Namespaces.PeerGroup.archive {
matchesGroup = !data.excludeArchived
} else {
matchesGroup = false
}
}
if matchesGroup && peerCount != 0 {
count -= 1
if !isMuted {
unmutedUnreadCount -= 1
}
}
}
}
}
}
result.append((filter, max(0, count), unmutedUnreadCount > 0))
}
return (totalBadge, result)
}
}
}