Ilya Laktyushin e5762bd9c8 Various fixes
2025-05-18 04:35:56 +04:00

403 lines
19 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import AvatarNode
import BundleIconComponent
import TelegramPresentationData
import TelegramCore
import AccountContext
import ListSectionComponent
import PlainButtonComponent
import ShimmerEffect
final class ChatbotSearchResultItemComponent: Component {
enum Content: Equatable {
case searching
case found(peer: EnginePeer, isInstalled: Bool)
case notFound
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let content: Content
let installAction: () -> Void
let removeAction: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
content: Content,
installAction: @escaping () -> Void,
removeAction: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.content = content
self.installAction = installAction
self.removeAction = removeAction
}
static func ==(lhs: ChatbotSearchResultItemComponent, rhs: ChatbotSearchResultItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.content != rhs.content {
return false
}
return true
}
final class View: UIView, ListSectionComponent.ChildView {
private var notFoundLabel: ComponentView<Empty>?
private let titleLabel = ComponentView<Empty>()
private let subtitleLabel = ComponentView<Empty>()
private var shimmerEffectNode: ShimmerEffectNode?
private var avatarNode: AvatarNode?
private var addButton: ComponentView<Empty>?
private var removeButton: ComponentView<Empty>?
private var component: ChatbotSearchResultItemComponent?
private weak var state: EmptyComponentState?
var customUpdateIsHighlighted: ((Bool) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ChatbotSearchResultItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let sideInset: CGFloat = 10.0
let avatarDiameter: CGFloat = 40.0
let avatarTextSpacing: CGFloat = 12.0
let titleSubtitleSpacing: CGFloat = 1.0
let verticalInset: CGFloat = 11.0
let maxTextWidth: CGFloat = availableSize.width - sideInset * 2.0 - avatarDiameter - avatarTextSpacing
var addButtonSize: CGSize?
if case .found(_, false) = component.content {
let addButton: ComponentView<Empty>
var addButtonTransition = transition
if let current = self.addButton {
addButton = current
} else {
addButtonTransition = addButtonTransition.withAnimation(.none)
addButton = ComponentView()
self.addButton = addButton
}
addButtonSize = addButton.update(
transition: addButtonTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.ChatbotSetup_BotAddAction, font: Font.semibold(15.0), textColor: component.theme.list.itemCheckColors.foregroundColor))
)),
background: AnyComponent(RoundedRectangle(color: component.theme.list.itemCheckColors.fillColor, cornerRadius: nil)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 8.0),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.installAction()
},
animateAlpha: true,
animateScale: false
)),
environment: {},
containerSize: CGSize(width: 140.0, height: 100.0)
)
} else {
if let addButton = self.addButton {
self.addButton = nil
if let addButtonView = addButton.view {
if !transition.animation.isImmediate {
transition.setScale(view: addButtonView, scale: 0.001)
ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: addButtonView, alpha: 0.0, completion: { [weak addButtonView] _ in
addButtonView?.removeFromSuperview()
})
} else {
addButtonView.removeFromSuperview()
}
}
}
}
var removeButtonSize: CGSize?
if case .found(_, true) = component.content {
let removeButton: ComponentView<Empty>
var removeButtonTransition = transition
if let current = self.removeButton {
removeButton = current
} else {
removeButtonTransition = removeButtonTransition.withAnimation(.none)
removeButton = ComponentView()
self.removeButton = removeButton
}
removeButtonSize = removeButton.update(
transition: removeButtonTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: "Chat/Message/SideCloseIcon",
tintColor: component.theme.list.controlSecondaryColor
)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.removeAction()
},
animateAlpha: true,
animateScale: false
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
} else {
if let removeButton = self.removeButton {
self.removeButton = nil
if let removeButtonView = removeButton.view {
if !transition.animation.isImmediate {
transition.setScale(view: removeButtonView, scale: 0.001)
ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: removeButtonView, alpha: 0.0, completion: { [weak removeButtonView] _ in
removeButtonView?.removeFromSuperview()
})
} else {
removeButtonView.removeFromSuperview()
}
}
}
}
let titleValue: String
let subtitleValue: String
let isTextVisible: Bool
switch component.content {
case .searching, .notFound:
isTextVisible = false
titleValue = "AAAAAAAAA"
subtitleValue = component.strings.Bot_GenericBotStatus
case let .found(peer, _):
isTextVisible = true
titleValue = peer.displayTitle(strings: component.strings, displayOrder: .firstLast)
subtitleValue = component.strings.Bot_GenericBotStatus
}
let titleSize = self.titleLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleValue, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
)
let subtitleSize = self.subtitleLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: subtitleValue, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
)
let size = CGSize(width: availableSize.width, height: verticalInset * 2.0 + titleSize.height + titleSubtitleSpacing + subtitleSize.height)
let titleFrame = CGRect(origin: CGPoint(x: sideInset + avatarDiameter + avatarTextSpacing, y: verticalInset), size: titleSize)
if let titleView = self.titleLabel.view {
var titleTransition = transition
if titleView.superview == nil {
titleTransition = .immediate
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
if titleView.isHidden != !isTextVisible {
titleTransition = .immediate
}
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
titleTransition.setPosition(view: titleView, position: titleFrame.origin)
titleView.isHidden = !isTextVisible
}
let subtitleFrame = CGRect(origin: CGPoint(x: sideInset + avatarDiameter + avatarTextSpacing, y: verticalInset + titleSize.height + titleSubtitleSpacing), size: subtitleSize)
if let subtitleView = self.subtitleLabel.view {
var subtitleTransition = transition
if subtitleView.superview == nil {
subtitleTransition = .immediate
subtitleView.layer.anchorPoint = CGPoint()
self.addSubview(subtitleView)
}
if subtitleView.isHidden != !isTextVisible {
subtitleTransition = .immediate
}
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
subtitleTransition.setPosition(view: subtitleView, position: subtitleFrame.origin)
subtitleView.isHidden = !isTextVisible
}
let avatarFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - avatarDiameter) * 0.5)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
if case let .found(peer, _) = component.content {
var avatarTransition = transition
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarTransition = .immediate
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0))
self.avatarNode = avatarNode
self.addSubview(avatarNode.view)
}
avatarTransition.setFrame(view: avatarNode.view, frame: avatarFrame)
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, synchronousLoad: true, displayDimensions: avatarFrame.size)
avatarNode.updateSize(size: avatarFrame.size)
} else {
if let avatarNode = self.avatarNode {
self.avatarNode = nil
avatarNode.view.removeFromSuperview()
}
}
if case .notFound = component.content {
let notFoundLabel: ComponentView<Empty>
if let current = self.notFoundLabel {
notFoundLabel = current
} else {
notFoundLabel = ComponentView()
self.notFoundLabel = notFoundLabel
}
let notFoundLabelSize = notFoundLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.ChatbotSetup_BotNotFoundStatus, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
)
let notFoundLabelFrame = CGRect(origin: CGPoint(x: floor((size.width - notFoundLabelSize.width) * 0.5), y: floor((size.height - notFoundLabelSize.height) * 0.5)), size: notFoundLabelSize)
if let notFoundLabelView = notFoundLabel.view {
var notFoundLabelTransition = transition
if notFoundLabelView.superview == nil {
notFoundLabelTransition = .immediate
self.addSubview(notFoundLabelView)
}
notFoundLabelTransition.setPosition(view: notFoundLabelView, position: notFoundLabelFrame.center)
notFoundLabelView.bounds = CGRect(origin: CGPoint(), size: notFoundLabelFrame.size)
}
} else {
if let notFoundLabel = self.notFoundLabel {
self.notFoundLabel = nil
notFoundLabel.view?.removeFromSuperview()
}
}
if let addButton = self.addButton, let addButtonSize {
var addButtonTransition = transition
let addButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - addButtonSize.width, y: floor((size.height - addButtonSize.height) * 0.5)), size: addButtonSize)
if let addButtonView = addButton.view {
if addButtonView.superview == nil {
addButtonTransition = addButtonTransition.withAnimation(.none)
self.addSubview(addButtonView)
if !transition.animation.isImmediate {
transition.animateScale(view: addButtonView, from: 0.001, to: 1.0)
ComponentTransition.easeInOut(duration: 0.2).animateAlpha(view: addButtonView, from: 0.0, to: 1.0)
}
}
addButtonTransition.setFrame(view: addButtonView, frame: addButtonFrame)
}
}
if let removeButton = self.removeButton, let removeButtonSize {
var removeButtonTransition = transition
let removeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - removeButtonSize.width, y: floor((size.height - removeButtonSize.height) * 0.5)), size: removeButtonSize)
if let removeButtonView = removeButton.view {
if removeButtonView.superview == nil {
removeButtonTransition = removeButtonTransition.withAnimation(.none)
self.addSubview(removeButtonView)
if !transition.animation.isImmediate {
transition.animateScale(view: removeButtonView, from: 0.001, to: 1.0)
ComponentTransition.easeInOut(duration: 0.2).animateAlpha(view: removeButtonView, from: 0.0, to: 1.0)
}
}
removeButtonTransition.setFrame(view: removeButtonView, frame: removeButtonFrame)
}
}
if case .searching = component.content {
let shimmerEffectNode: ShimmerEffectNode
if let current = self.shimmerEffectNode {
shimmerEffectNode = current
} else {
shimmerEffectNode = ShimmerEffectNode()
self.shimmerEffectNode = shimmerEffectNode
self.addSubview(shimmerEffectNode.view)
}
shimmerEffectNode.frame = CGRect(origin: CGPoint(), size: size)
shimmerEffectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = titleFrame.width
let subtitleLineWidth: CGFloat = subtitleFrame.width
let lineDiameter: CGFloat = 10.0
shapes.append(.circle(avatarFrame))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: lineDiameter))
shimmerEffectNode.update(backgroundColor: component.theme.list.itemBlocksBackgroundColor, foregroundColor: component.theme.list.mediaPlaceholderColor, shimmeringColor: component.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: size)
} else {
if let shimmerEffectNode = self.shimmerEffectNode {
self.shimmerEffectNode = nil
shimmerEffectNode.view.removeFromSuperview()
}
}
self.separatorInset = 16.0
return 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)
}
}