mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-07-30 19:21:43 +02:00
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:
@@ -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",
|
||||
],
|
||||
)
|
||||
+1127
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
],
|
||||
)
|
||||
+139
@@ -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)
|
||||
}
|
||||
}
|
||||
+548
@@ -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",
|
||||
],
|
||||
)
|
||||
+295
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+206
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user