Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,43 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatFolderLinkPreviewScreen",
module_name = "ChatFolderLinkPreviewScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/AnimatedCounterComponent",
"//submodules/AvatarNode",
"//submodules/CheckNode",
"//submodules/Markdown",
"//submodules/UndoUI",
"//submodules/PremiumUI",
"//submodules/QrCodeUI",
"//submodules/InviteLinksUI",
"//submodules/Components/MultilineTextWithEntitiesComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,196 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
final class ActionListItemComponent: Component {
let theme: PresentationTheme
let sideInset: CGFloat
let iconName: String?
let title: String
let hasNext: Bool
let action: () -> Void
init(
theme: PresentationTheme,
sideInset: CGFloat,
iconName: String?,
title: String,
hasNext: Bool,
action: @escaping () -> Void
) {
self.theme = theme
self.sideInset = sideInset
self.iconName = iconName
self.title = title
self.hasNext = hasNext
self.action = action
}
static func ==(lhs: ActionListItemComponent, rhs: ActionListItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
final class View: UIView {
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
private let iconView: UIImageView
private let separatorLayer: SimpleLayer
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
private var component: ActionListItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.containerButton = HighlightTrackingButton()
self.iconView = UIImageView()
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.containerButton)
self.containerButton.addSubview(self.iconView)
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
func update(component: ActionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
if self.component?.iconName != component.iconName {
if let iconName = component.iconName {
self.iconView.image = UIImage(bundleImageName: iconName)?.withRenderingMode(.alwaysTemplate)
} else {
self.iconView.image = nil
}
}
if themeUpdated {
self.iconView.tintColor = component.theme.list.itemAccentColor
}
self.component = component
self.state = state
let contextInset: CGFloat = 0.0
let height: CGFloat = 44.0
let verticalInset: CGFloat = 1.0
let leftInset: CGFloat = 62.0 + component.sideInset
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
let previousTitleFrame = self.title.view?.frame
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let centralContentHeight: CGFloat = titleSize.height
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
}
}
if let iconImage = self.iconView.image {
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - iconImage.size.width) / 2.0), y: floor((height - iconImage.size.height) / 2.0)), size: iconImage.size))
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
self.separatorLayer.isHidden = !component.hasNext
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
transition.setFrame(view: self.containerButton, frame: containerFrame)
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,335 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import AccountContext
import MultilineTextComponent
import TelegramPresentationData
import TelegramCore
import MultilineTextWithEntitiesComponent
final class BadgeComponent: Component {
let fillColor: UIColor
let content: AnyComponent<Empty>
init(
fillColor: UIColor,
content: AnyComponent<Empty>
) {
self.fillColor = fillColor
self.content = content
}
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
if lhs.fillColor != rhs.fillColor {
return false
}
if lhs.content != rhs.content {
return false
}
return true
}
final class View: UIView {
private let backgroundLayer: SimpleLayer
private let content = ComponentView<Empty>()
override init(frame: CGRect) {
self.backgroundLayer = SimpleLayer()
super.init(frame: frame)
self.layer.addSublayer(self.backgroundLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let height: CGFloat = 20.0
let contentInset: CGFloat = 10.0
let contentSize = self.content.update(
transition: transition,
component: component.content,
environment: {},
containerSize: availableSize
)
let backgroundWidth: CGFloat = max(height, contentSize.width + contentInset)
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundWidth, height: height))
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
transition.setBackgroundColor(layer: self.backgroundLayer, color: component.fillColor)
transition.setCornerRadius(layer: self.backgroundLayer, cornerRadius: height / 2.0)
if let contentView = self.content.view {
if contentView.superview == nil {
self.addSubview(contentView)
}
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - contentSize.width) * 0.5), y: floor((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize))
}
return backgroundFrame.size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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)
}
}
final class ChatFolderLinkHeaderComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let title: ChatFolderTitle
let badge: String?
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
title: ChatFolderTitle,
badge: String?
) {
self.context = context
self.theme = theme
self.strings = strings
self.title = title
self.badge = badge
}
static func ==(lhs: ChatFolderLinkHeaderComponent, rhs: ChatFolderLinkHeaderComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.badge != rhs.badge {
return false
}
return true
}
final class View: UIView {
private let leftView = UIImageView()
private let rightView = UIImageView()
private let title = ComponentView<Empty>()
private let separatorLayer = SimpleLayer()
private var badge: ComponentView<Empty>?
private var component: ChatFolderLinkHeaderComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.leftView)
self.addSubview(self.rightView)
self.layer.addSublayer(self.separatorLayer)
self.separatorLayer.cornerRadius = 2.0
self.separatorLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ChatFolderLinkHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
let height: CGFloat = 60.0
let spacing: CGFloat = 16.0
let badgeSpacing: CGFloat = 6.0
if themeUpdated {
let leftString = NSAttributedString(string: component.strings.FolderLinkPreview_IconTabLeft, font: Font.semibold(14.0), textColor: component.theme.list.freeTextColor)
let rightString = NSAttributedString(string: component.strings.FolderLinkPreview_IconTabRight, font: Font.semibold(14.0), textColor: component.theme.list.freeTextColor)
let leftStringBounds = leftString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let rightStringBounds = rightString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
self.leftView.image = generateImage(leftStringBounds.size.integralFloor, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
leftString.draw(in: leftStringBounds)
var colors: [UIColor] = []
var locations: [CGFloat] = []
for i in 0 ... 8 {
let t: CGFloat = CGFloat(i) / CGFloat(8)
let a: CGFloat = t * t
colors.append(UIColor(white: 1.0, alpha: a))
locations.append(t)
}
if let image = generateGradientImage(size: CGSize(width: size.width * 0.8, height: 16.0), colors: colors, locations: locations, direction: .horizontal) {
image.draw(in: CGRect(origin: CGPoint(), size: CGSize(width: image.size.width, height: size.height)), blendMode: .destinationIn, alpha: 1.0)
}
UIGraphicsPopContext()
})
self.leftView.alpha = 0.5
self.rightView.image = generateImage(rightStringBounds.size.integralFloor, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
rightString.draw(in: rightStringBounds)
var colors: [UIColor] = []
var locations: [CGFloat] = []
for i in 0 ... 8 {
let t: CGFloat = CGFloat(i) / CGFloat(8)
let a: CGFloat = 1.0 - t * t
colors.append(UIColor(white: 1.0, alpha: a))
locations.append(t)
}
if let image = generateGradientImage(size: CGSize(width: size.width * 0.8, height: 16.0), colors: colors, locations: locations, direction: .horizontal) {
image.draw(in: CGRect(origin: CGPoint(x: size.width - image.size.width, y: 0.0), size: CGSize(width: image.size.width, height: size.height)), blendMode: .destinationIn, alpha: 1.0)
}
UIGraphicsPopContext()
})
self.rightView.alpha = 0.5
self.separatorLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor
}
var contentWidth: CGFloat = 0.0
if let leftImage = self.leftView.image {
contentWidth += leftImage.size.width
}
contentWidth += spacing
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
text: .plain(component.title.attributedString(font: Font.semibold(17.0), textColor: component.theme.list.itemAccentColor)),
manualVisibilityControl: false
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
contentWidth += titleSize.width
var badgeSize: CGSize?
if let badge = component.badge {
let badgeContainer: ComponentView<Empty>
var badgeTransition = transition
if let current = self.badge {
badgeContainer = current
} else {
badgeTransition = .immediate
badgeContainer = ComponentView()
self.badge = badgeContainer
}
let badgeSizeValue = badgeContainer.update(
transition: badgeTransition,
component: AnyComponent(BadgeComponent(
fillColor: component.theme.list.itemCheckColors.fillColor,
content: AnyComponent(Text(text: badge, font: Font.semibold(12.0), color: component.theme.list.itemCheckColors.foregroundColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
badgeSize = badgeSizeValue
contentWidth += badgeSpacing + badgeSizeValue.width
} else {
if let badge = self.badge {
self.badge = nil
if let view = badge.view {
transition.setScale(view: view, scale: 0.0001, completion: { [weak view] _ in
view?.removeFromSuperview()
})
}
}
}
contentWidth += spacing
if let rightImage = self.rightView.image {
contentWidth += rightImage.size.width
}
var contentOriginX: CGFloat = 0.0
if let leftImage = self.leftView.image {
transition.setFrame(view: self.leftView, frame: CGRect(origin: CGPoint(x: contentOriginX, y: floor((height - leftImage.size.height) / 2.0) - 1.0), size: leftImage.size))
contentOriginX += leftImage.size.width
}
contentOriginX += spacing
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
let titleFrame = CGRect(origin: CGPoint(x: contentOriginX, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
transition.setFrame(view: titleView, frame: titleFrame)
var separatorWidth = titleFrame.width
if let badgeSize, let badge = self.badge {
let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: titleFrame.minY + 1.0 + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize)
separatorWidth += badgeSpacing + badgeSize.width
if let badgeView = badge.view {
var badgeTransition = transition
var animateIn = false
if badgeView.superview == nil {
badgeTransition = .immediate
self.addSubview(badgeView)
animateIn = true
}
badgeTransition.setFrame(view: badgeView, frame: badgeFrame)
if animateIn {
transition.animateScale(view: badgeView, from: 0.0001, to: 1.0)
}
}
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + 9.0), size: CGSize(width: separatorWidth, height: 3.0)))
}
contentOriginX += titleSize.width
if let badgeSize {
contentOriginX += badgeSpacing
contentOriginX += badgeSize.width
}
contentOriginX += spacing
if let rightImage = self.rightView.image {
transition.setFrame(view: self.rightView, frame: CGRect(origin: CGPoint(x: contentOriginX, y: floor((height - rightImage.size.height) / 2.0) - 1.0), size: rightImage.size))
contentOriginX += rightImage.size.width
}
return CGSize(width: contentWidth, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,330 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import TelegramCore
import CheckNode
func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gesture in gestureRecognizers {
if let gesture = gesture as? ContextGesture {
gesture.cancel()
}
}
}
for subview in view.subviews {
cancelContextGestures(view: subview)
}
}
final class LinkListItemComponent: Component {
enum SelectionState: Equatable {
case none
case editing(isSelected: Bool)
}
let theme: PresentationTheme
let sideInset: CGFloat
let title: String
let link: ExportedChatFolderLink
let label: String
let selectionState: SelectionState
let hasNext: Bool
let action: (ExportedChatFolderLink) -> Void
let contextAction: (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void
init(
theme: PresentationTheme,
sideInset: CGFloat,
title: String,
link: ExportedChatFolderLink,
label: String,
selectionState: SelectionState,
hasNext: Bool,
action: @escaping (ExportedChatFolderLink) -> Void,
contextAction: @escaping (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void
) {
self.theme = theme
self.sideInset = sideInset
self.title = title
self.link = link
self.label = label
self.selectionState = selectionState
self.hasNext = hasNext
self.action = action
self.contextAction = contextAction
}
static func ==(lhs: LinkListItemComponent, rhs: LinkListItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.link != rhs.link {
return false
}
if lhs.label != rhs.label {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
final class View: ContextControllerSourceView {
private let extractedContainerView: ContextExtractedContentContainingView
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private let iconView: UIImageView
private let iconBackgroundView: UIImageView
private var checkLayer: CheckLayer?
private var isExtractedToContextMenu: Bool = false
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
private var component: LinkListItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.extractedContainerView = ContextExtractedContentContainingView()
self.containerButton = HighlightTrackingButton()
self.iconView = UIImageView()
self.iconBackgroundView = UIImageView()
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.extractedContainerView.contentView.addSubview(self.containerButton)
self.containerButton.addSubview(self.iconBackgroundView)
self.containerButton.addSubview(self.iconView)
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
guard let self, let component = self.component else {
return
}
self.containerButton.clipsToBounds = value
self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
}
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
guard let self else {
return
}
self.isExtractedToContextMenu = value
let mappedTransition: ComponentTransition
if value {
mappedTransition = ComponentTransition(transition)
} else {
mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))
}
self.state?.updated(transition: mappedTransition)
}
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted, case .none = component.selectionState {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction(component.link, self.extractedContainerView, gesture)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action(component.link)
}
func update(component: LinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
self.state = state
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
let height: CGFloat = 60.0
let verticalInset: CGFloat = 1.0
var leftInset: CGFloat = 62.0 + component.sideInset
var iconLeftInset: CGFloat = component.sideInset
if case let .editing(isSelected) = component.selectionState {
leftInset += 48.0
iconLeftInset += 48.0
let checkSize: CGFloat = 22.0
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
if themeUpdated {
checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain)
}
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
} else {
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
self.checkLayer = checkLayer
self.containerButton.layer.addSublayer(checkLayer)
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
checkLayer.setSelected(isSelected, animated: false)
checkLayer.setNeedsDisplay()
}
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: component.sideInset + 20.0, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
} else {
if let checkLayer = self.checkLayer {
self.checkLayer = nil
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
checkLayer?.removeFromSuperlayer()
})
}
}
let rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset
if themeUpdated {
self.iconBackgroundView.image = generateFilledCircleImage(diameter: 40.0, color: component.theme.list.itemCheckColors.fillColor)
self.iconView.image = UIImage(bundleImageName: "Chat/Context Menu/Link")?.withRenderingMode(.alwaysTemplate)
self.iconView.tintColor = component.theme.list.itemCheckColors.foregroundColor
}
if let iconImage = self.iconView.image {
transition.setFrame(view: self.iconBackgroundView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - 40.0) * 0.5), y: floor((height - 40.0) * 0.5)), size: CGSize(width: 40.0, height: 40.0)))
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconImage.size.width) * 0.5), y: floor((height - iconImage.size.height) * 0.5)), size: iconImage.size))
}
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.label, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let titleSpacing: CGFloat = 1.0
let contentHeight: CGFloat = titleSize.height + titleSpacing + labelSize.height
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - contentHeight) / 2.0)), size: titleSize)
let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
titleView.layer.anchorPoint = CGPoint()
self.containerButton.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
labelView.layer.anchorPoint = CGPoint()
self.containerButton.addSubview(labelView)
}
transition.setPosition(view: labelView, position: labelFrame.origin)
labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size)
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
self.separatorLayer.isHidden = !component.hasNext
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
self.extractedContainerView.contentRect = resultBounds
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
transition.setFrame(view: self.containerButton, frame: containerFrame)
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,312 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramCore
import MultilineTextComponent
import AvatarNode
import TelegramPresentationData
import CheckNode
import TelegramStringFormatting
private let avatarFont = avatarPlaceholderFont(size: 15.0)
final class PeerListItemComponent: Component {
enum SelectionState: Equatable {
case none
case editing(isSelected: Bool, isTinted: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let sideInset: CGFloat
let title: String
let peer: EnginePeer?
let subtitle: String?
let selectionState: SelectionState
let hasNext: Bool
let action: (EnginePeer) -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
sideInset: CGFloat,
title: String,
peer: EnginePeer?,
subtitle: String?,
selectionState: SelectionState,
hasNext: Bool,
action: @escaping (EnginePeer) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.sideInset = sideInset
self.title = title
self.peer = peer
self.subtitle = subtitle
self.selectionState = selectionState
self.hasNext = hasNext
self.action = action
}
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
final class View: UIView {
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private let avatarNode: AvatarNode
private var checkLayer: CheckLayer?
private var component: PeerListItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.containerButton = HighlightTrackingButton()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = true
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.containerButton)
self.containerButton.layer.addSublayer(self.avatarNode.layer)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component, let peer = component.peer else {
return
}
component.action(peer)
}
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
var hasSelectionUpdated = false
if let previousComponent = self.component {
switch previousComponent.selectionState {
case .none:
if case .none = component.selectionState {
} else {
hasSelectionUpdated = true
}
case .editing:
if case .editing = component.selectionState {
} else {
hasSelectionUpdated = true
}
}
}
self.component = component
self.state = state
let contextInset: CGFloat = 0.0
let height: CGFloat = 60.0
let verticalInset: CGFloat = 1.0
var leftInset: CGFloat = 62.0 + component.sideInset
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
var avatarLeftInset: CGFloat = component.sideInset + 10.0
if case let .editing(isSelected, isTinted) = component.selectionState {
leftInset += 44.0
avatarLeftInset += 44.0
let checkSize: CGFloat = 22.0
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
if themeUpdated {
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
}
checkLayer.theme = theme
}
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
} else {
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
}
checkLayer = CheckLayer(theme: theme)
self.checkLayer = checkLayer
self.containerButton.layer.addSublayer(checkLayer)
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
checkLayer.setSelected(isSelected, animated: false)
checkLayer.setNeedsDisplay()
}
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
} else {
if let checkLayer = self.checkLayer {
self.checkLayer = nil
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
checkLayer?.removeFromSuperlayer()
})
}
}
let avatarSize: CGFloat = 40.0
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
if self.avatarNode.bounds.isEmpty {
self.avatarNode.frame = avatarFrame
} else {
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
}
if let peer = component.peer {
let clipStyle: AvatarNodeClipStyle
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
clipStyle = .roundedRect
} else {
clipStyle = .round
}
if peer.id == component.context.account.peerId {
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
} else {
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
}
}
let labelData: (String, Bool)
if let subtitle = component.subtitle {
labelData = (subtitle, false)
} else if case .legacyGroup = component.peer {
labelData = (component.strings.Group_Status, false)
} else if case let .channel(channel) = component.peer {
if case .group = channel.info {
labelData = (component.strings.Group_Status, false)
} else {
labelData = (component.strings.Channel_Status, false)
}
} else {
labelData = (component.strings.Group_Status, false)
}
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let previousTitleFrame = self.title.view?.frame
var previousTitleContents: UIView?
if hasSelectionUpdated && !"".isEmpty {
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let titleSpacing: CGFloat = 1.0
let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
}
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
self.addSubview(previousTitleContents)
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
previousTitleContents?.removeFromSuperview()
})
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
}
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
self.containerButton.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize))
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
self.separatorLayer.isHidden = !component.hasNext
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
transition.setFrame(view: self.containerButton, frame: containerFrame)
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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)
}
}