chore: migrate to new version + fixed several critical bugs

- Migrated project to latest Telegram iOS base (v12.3.2+)
- Fixed circular dependency between GhostModeManager and MiscSettingsManager
- Fixed multiple Bazel build configuration errors (select() default conditions)
- Fixed duplicate type definitions in PeerInfoScreen
- Fixed swiftmodule directory resolution in build scripts
- Added Ghostgram Settings tab in main Settings menu with all 5 features
- Cleared sensitive credentials from config.json (template-only now)
- Excluded bazel-cache from version control
This commit is contained in:
ichmagmaus 812
2026-02-23 23:04:32 +01:00
parent 703e291bcb
commit db53826061
1017 changed files with 62323 additions and 40545 deletions
@@ -0,0 +1,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatListFilterTabContainerNode",
module_name = "ChatListFilterTabContainerNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/LiquidLens",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatListHeaderNoticeComponent",
module_name = "ChatListHeaderNoticeComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AppBundle",
"//submodules/ItemListUI",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TextFormat",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/GlobalControlPanelsContext",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,139 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import AccountContext
import GlobalControlPanelsContext
import ComponentFlow
import ComponentDisplayAdapters
public final class ChatListHeaderNoticeComponent: Component {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let data: GlobalControlPanelsContext.ChatListNotice
public let activateAction: (GlobalControlPanelsContext.ChatListNotice) -> Void
public let dismissAction: (GlobalControlPanelsContext.ChatListNotice) -> Void
public let selectAction: (GlobalControlPanelsContext.ChatListNotice, Bool) -> Void
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
data: GlobalControlPanelsContext.ChatListNotice,
activateAction: @escaping (GlobalControlPanelsContext.ChatListNotice) -> Void,
dismissAction: @escaping (GlobalControlPanelsContext.ChatListNotice) -> Void,
selectAction: @escaping (GlobalControlPanelsContext.ChatListNotice, Bool) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.data = data
self.activateAction = activateAction
self.dismissAction = dismissAction
self.selectAction = selectAction
}
public static func ==(lhs: ChatListHeaderNoticeComponent, rhs: ChatListHeaderNoticeComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.data != rhs.data {
return false
}
return true
}
public final class View: UIView {
private var panel: ChatListNoticeItemNode?
private var component: ChatListHeaderNoticeComponent?
private weak var state: EmptyComponentState?
public override init(frame: CGRect) {
super.init(frame: frame)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
if case .ended = recognizer.state {
component.activateAction(component.data)
}
}
func update(component: ChatListHeaderNoticeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let itemNode: ChatListNoticeItemNode
if let current = self.panel {
itemNode = current
} else {
itemNode = ChatListNoticeItemNode()
self.panel = itemNode
self.addSubview(itemNode.view)
}
let item = ChatListNoticeItem(
context: component.context,
theme: component.theme,
strings: component.strings,
notice: component.data,
action: { [weak self] action in
guard let self, let component = self.component else {
return
}
switch action {
case .activate:
component.activateAction(component.data)
case .hide:
component.dismissAction(component.data)
case let .buttonChoice(isPositive):
component.selectAction(component.data, isPositive)
}
}
)
let (nodeLayout, apply) = itemNode.asyncLayout()(item, ListViewItemLayoutParams(
width: availableSize.width,
leftInset: 0.0,
rightInset: 0.0,
availableHeight: 10000.0,
isStandalone: true
), false)
let size = CGSize(width: availableSize.width, height: nodeLayout.contentSize.height)
let panelFrame = CGRect(origin: CGPoint(), size: size)
transition.setFrame(view: itemNode.view, frame: panelFrame)
apply()
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,548 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AppBundle
import ItemListUI
import Markdown
import AccountContext
import MergedAvatarsNode
import TextNodeWithEntities
import TextFormat
import AvatarNode
import GlobalControlPanelsContext
class ChatListNoticeItem: ListViewItem {
enum Action {
case activate
case hide
case buttonChoice(isPositive: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let notice: GlobalControlPanelsContext.ChatListNotice
let action: (Action) -> Void
let selectable: Bool = true
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, notice: GlobalControlPanelsContext.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, 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)
}
@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.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,26 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatListSearchFiltersContainerNode",
module_name = "ChatListSearchFiltersContainerNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,295 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import GlassBackgroundComponent
import ComponentFlow
import ComponentDisplayAdapters
public enum ChatListSearchFilterEntryId: Hashable {
case filter(Int64)
}
public enum ChatListSearchFilterEntry: Equatable {
case filter(ChatListSearchFilter)
public var id: ChatListSearchFilterEntryId {
switch self {
case let .filter(filter):
return .filter(filter.id)
}
}
}
public final class ChatListSearchFiltersContainerNode: ASDisplayNode {
private let backgroundContainer: GlassBackgroundContainerView
private let backgroundView: GlassBackgroundView
private let scrollNode: ASScrollNode
private let selectionView: UIImageView
private var itemNodes: [ChatListSearchFilterEntryId: ItemNode] = [:]
public 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 public init() {
self.backgroundContainer = GlassBackgroundContainerView()
self.backgroundView = GlassBackgroundView()
self.backgroundContainer.contentView.addSubview(self.backgroundView)
self.scrollNode = ASScrollNode()
self.selectionView = UIImageView()
super.init()
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
self.view.addSubview(self.backgroundContainer)
self.backgroundView.contentView.addSubview(self.scrollNode.view)
self.scrollNode.view.addSubview(self.selectionView)
}
public func cancelAnimations() {
self.scrollNode.layer.removeAllAnimations()
}
public 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
let componentTransition = ComponentTransition(transition)
componentTransition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundContainer.update(size: size, isDark: presentationData.theme.overallDarkAppearance, transition: componentTransition)
componentTransition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: componentTransition)
self.scrollNode.view.layer.cornerRadius = size.height * 0.5
var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter
let previousScrollBounds = self.scrollNode.bounds
let previousContentWidth = self.scrollNode.view.contentSize.width
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.view.convert(paneNode.frame, to: self.view)) && self.bounds.intersects(self.scrollNode.view.convert(paneFrame, to: self.view)) {
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.view.convert(paneFrame, to: self.view)) {
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 {
let wasAdded = self.selectionView.isHidden
let selectionFrame = CGRect(origin: CGPoint(x: selectedFrame.minX - 13.0, y: 3.0), size: CGSize(width: selectedFrame.width + 26.0, height: size.height - 3.0 * 2.0))
if wasAdded {
self.selectionView.frame = selectionFrame
ComponentTransition(transition).animateAlpha(view: self.selectionView, from: 0.0, to: 1.0)
} else {
transition.updateFrame(view: self.selectionView, frame: selectionFrame)
}
self.selectionView.isHidden = false
if self.selectionView.image?.size.height != selectionFrame.height {
self.selectionView.image = generateStretchableFilledCircleImage(diameter: selectionFrame.height, color: .white)?.withRenderingMode(.alwaysTemplate)
}
self.selectionView.tintColor = presentationData.theme.chat.inputPanel.panelControlColor.withAlphaComponent(0.1)
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 {
self.selectionView.isHidden = true
self.previousSelectedAbsFrame = nil
self.previousSelectedFrame = nil
}
if updated && self.scrollNode.view.contentOffset.x > 0.0 {
self.scrollNode.view.contentOffset = CGPoint()
}
}
}
@@ -0,0 +1,206 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import TelegramCore
import AccountContext
final class ItemNode: ASDisplayNode {
private let pressed: () -> Void
private let iconNode: ASImageNode
private let titleNode: 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.buttonNode = HighlightTrackingButtonNode()
super.init()
self.addSubnode(self.titleNode)
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.chat.inputPanel.panelControlColor
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(15.0), textColor: color)
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()
}
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: 4.0 + 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 titleFrame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left + iconInset, y: self.titleNode.insets.top + floorToScreenPixels((height - titleSize.height) / 2.0)), size: titleSize)
self.titleNode.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)
}
}