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,42 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatbotSetupScreen",
module_name = "ChatbotSetupScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/BackButtonComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/ShimmerEffect",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,721 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import TelegramCore
import AccountContext
import ListSectionComponent
import ListActionItemComponent
import PeerListItemComponent
import ViewControllerComponent
import BundleIconComponent
import AvatarNode
import SwiftSignalKit
final class BusinessRecipientListScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let initialData: BusinessRecipientListScreen.InitialData
let mode: BusinessRecipientListScreen.Mode
let update: (BusinessRecipientListScreenComponent.PeerList) -> Void
init(
context: AccountContext,
initialData: BusinessRecipientListScreen.InitialData,
mode: BusinessRecipientListScreen.Mode,
update: @escaping (BusinessRecipientListScreenComponent.PeerList) -> Void
) {
self.context = context
self.initialData = initialData
self.mode = mode
self.update = update
}
static func ==(lhs: BusinessRecipientListScreenComponent, rhs: BusinessRecipientListScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
struct PeerList {
enum Category: Int {
case newChats = 0
case existingChats = 1
case contacts = 2
case nonContacts = 3
}
struct Peer {
var peer: EnginePeer
var isContact: Bool
init(peer: EnginePeer, isContact: Bool) {
self.peer = peer
self.isContact = isContact
}
}
var categories: Set<Category>
var peers: [Peer]
init(categories: Set<Category>, peers: [Peer]) {
self.categories = categories
self.peers = peers
}
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
private let navigationTitle = ComponentView<Empty>()
private let excludedSection = ComponentView<Empty>()
private let clearSection = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: BusinessRecipientListScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var peerList = PeerList(
categories: Set(),
peers: []
)
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.alwaysBounceVertical = true
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
guard let component = self.component else {
return true
}
component.update(self.peerList)
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
var scrolledUp = true
private func updateScrolling(transition: ComponentTransition) {
let navigationRevealOffsetY: CGFloat = 0.0
let navigationAlphaDistance: CGFloat = 16.0
let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance))
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
}
var scrolledUp = false
if navigationAlpha < 0.5 {
scrolledUp = true
} else if navigationAlpha > 0.5 {
scrolledUp = false
}
if self.scrolledUp != scrolledUp {
self.scrolledUp = scrolledUp
if !self.isUpdating {
self.state?.updated()
}
}
if let navigationTitleView = self.navigationTitle.view {
transition.setAlpha(view: navigationTitleView, alpha: 1.0)
}
}
static func makePeerListSetupScreen(context: AccountContext, mode: BusinessRecipientListScreen.Mode, initialPeerList: BusinessRecipientListScreenComponent.PeerList, completion: @escaping (BusinessRecipientListScreenComponent.PeerList) -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
enum AdditionalCategoryId: Int {
case existingChats
case newChats
case contacts
case nonContacts
}
let hasAccessToAllChatsByDefault: Bool
let isExclude: Bool
switch mode {
case .excludeExceptions:
hasAccessToAllChatsByDefault = true
isExclude = false
case .includeExceptions:
hasAccessToAllChatsByDefault = false
isExclude = false
case .excludeUsers:
hasAccessToAllChatsByDefault = false
isExclude = true
}
let additionalCategories: [ChatListNodeAdditionalCategory]
var selectedCategories = Set<Int>()
if isExclude {
additionalCategories = []
} else {
additionalCategories = [
ChatListNodeAdditionalCategory(
id: hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), cornerRadius: 12.0, color: .purple),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .purple),
title: hasAccessToAllChatsByDefault ? presentationData.strings.BusinessMessageSetup_Recipients_CategoryExistingChats : presentationData.strings.BusinessMessageSetup_Recipients_CategoryNewChats
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.contacts.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
title: presentationData.strings.BusinessMessageSetup_Recipients_CategoryContacts
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.nonContacts.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow),
title: presentationData.strings.BusinessMessageSetup_Recipients_CategoryNonContacts
)
]
}
if !isExclude {
for category in initialPeerList.categories {
switch category {
case .existingChats:
selectedCategories.insert(AdditionalCategoryId.existingChats.rawValue)
case .newChats:
selectedCategories.insert(AdditionalCategoryId.newChats.rawValue)
case .contacts:
selectedCategories.insert(AdditionalCategoryId.contacts.rawValue)
case .nonContacts:
selectedCategories.insert(AdditionalCategoryId.nonContacts.rawValue)
}
}
}
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: presentationData.strings.ChatbotSetup_Recipients_SelectionTitle,
searchPlaceholder: presentationData.strings.ChatListFilter_AddChatsSearchPlaceholder,
selectedChats: Set(initialPeerList.peers.map(\.peer.id)),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
chatListFilters: nil,
onlyUsers: true
)), filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in
}))
controller.navigationPresentation = .modal
let _ = (controller.result
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
guard case let .result(rawPeerIds, additionalCategoryIds) = result else {
controller?.dismiss()
return
}
let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in
switch id {
case let .peer(id):
return id
case .deviceContact:
return nil
}
}
let _ = (context.engine.data.get(
EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))
),
EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:))
)
)
|> deliverOnMainQueue).start(next: { peerMap, isContactMap in
var peerList = BusinessRecipientListScreenComponent.PeerList(categories: Set(), peers: [])
if !isExclude {
let mappedCategories = additionalCategoryIds.compactMap { item -> PeerList.Category? in
switch item {
case AdditionalCategoryId.existingChats.rawValue:
return .existingChats
case AdditionalCategoryId.newChats.rawValue:
return .newChats
case AdditionalCategoryId.contacts.rawValue:
return .contacts
case AdditionalCategoryId.nonContacts.rawValue:
return .nonContacts
default:
return nil
}
}
peerList.categories = Set(mappedCategories)
peerList.peers.removeAll()
for id in peerIds {
guard let maybePeer = peerMap[id], let peer = maybePeer else {
continue
}
peerList.peers.append(PeerList.Peer(
peer: peer,
isContact: isContactMap[id] ?? false
))
}
peerList.peers.sort(by: { lhs, rhs in
return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle
})
} else {
peerList.peers.removeAll()
for id in peerIds {
guard let maybePeer = peerMap[id], let peer = maybePeer else {
continue
}
peerList.peers.append(PeerList.Peer(
peer: peer,
isContact: isContactMap[id] ?? false
))
}
peerList.peers.sort(by: { lhs, rhs in
return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle
})
}
controller?.dismiss()
completion(peerList)
})
})
return controller
}
private func openPeerListSetup() {
guard let component = self.component, let environment = self.environment else {
return
}
let controller = BusinessRecipientListScreenComponent.View.makePeerListSetupScreen(
context: component.context,
mode: component.mode,
initialPeerList: self.peerList,
completion: { [weak self] peerList in
guard let self else {
return
}
self.peerList = peerList
self.state?.updated(transition: .immediate)
}
)
environment.controller()?.push(controller)
}
func update(component: BusinessRecipientListScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.peerList = component.initialData.peerList
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25)
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let title: String
switch component.mode {
case .excludeExceptions, .excludeUsers:
title = environment.strings.ChatbotSetup_Recipients_ExcludedListTitle
case .includeExceptions:
title = environment.strings.ChatbotSetup_Recipients_IncludedListTitle
}
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)
if let navigationTitleView = self.navigationTitle.view {
if navigationTitleView.superview == nil {
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
navigationBar.view.addSubview(navigationTitleView)
}
}
transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame)
}
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 32.0
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
contentHeight += 16.0
var excludedSectionItems: [AnyComponentWithIdentity<Empty>] = []
excludedSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.ChatbotSetup_Recipients_AddUsers,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
name: "Chat List/AddIcon",
tintColor: environment.theme.list.itemAccentColor
))), false),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openPeerListSetup()
}
))))
for category in self.peerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) {
let title: String
let icon: String
let color: AvatarBackgroundColor
switch category {
case .newChats:
title = environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats
icon = "Chat List/Filters/NewChats"
color = .purple
case .existingChats:
title = environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats
icon = "Chat List/Filters/Chats"
color = .purple
case .contacts:
title = environment.strings.BusinessMessageSetup_Recipients_CategoryContacts
icon = "Chat List/Filters/Contact"
color = .blue
case .nonContacts:
title = environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts
icon = "Chat List/Filters/User"
color = .yellow
}
excludedSectionItems.append(AnyComponentWithIdentity(id: category, component: AnyComponent(PeerListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
style: .generic,
sideInset: 0.0,
title: title,
avatar: PeerListItemComponent.Avatar(
icon: icon,
color: color,
clipStyle: .roundedRect
),
peer: nil,
subtitle: nil,
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: false,
action: { peer, _, _ in
},
inlineActions: PeerListItemComponent.InlineActionsState(
actions: [PeerListItemComponent.InlineAction(
id: AnyHashable(0),
title: environment.strings.Common_Delete,
color: .destructive,
action: { [weak self] in
guard let self else {
return
}
self.peerList.categories.remove(category)
self.state?.updated(transition: .spring(duration: 0.4))
if self.peerList.categories.isEmpty && self.peerList.peers.isEmpty {
let _ = self.attemptNavigation(complete: {})
self.environment?.controller()?.dismiss()
}
}
)]
)
))))
}
for peer in self.peerList.peers {
excludedSectionItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(PeerListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
style: .generic,
sideInset: 0.0,
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: peer.peer,
subtitle: PeerListItemComponent.Subtitle(text: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser, color: .neutral),
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: false,
action: { peer, _, _ in
},
inlineActions: PeerListItemComponent.InlineActionsState(
actions: [PeerListItemComponent.InlineAction(
id: AnyHashable(0),
title: environment.strings.Common_Delete,
color: .destructive,
action: { [weak self] in
guard let self else {
return
}
self.peerList.peers.removeAll(where: { $0.peer.id == peer.peer.id })
self.state?.updated(transition: .spring(duration: 0.4))
if self.peerList.categories.isEmpty && self.peerList.peers.isEmpty {
let _ = self.attemptNavigation(complete: {})
self.environment?.controller()?.dismiss()
}
}
)]
)
))))
}
let excludedSectionSize = self.excludedSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
style: .glass,
header: nil,
footer: nil,
items: excludedSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let excludedSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: excludedSectionSize)
if let excludedSectionView = self.excludedSection.view {
if excludedSectionView.superview == nil {
self.scrollView.addSubview(excludedSectionView)
}
transition.setFrame(view: excludedSectionView, frame: excludedSectionFrame)
}
contentHeight += excludedSectionSize.height
contentHeight += sectionSpacing
let clearSectionSize = self.clearSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
style: .glass,
header: nil,
footer: nil,
items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.ChatbotSetup_Recipients_RemoveAll,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemDestructiveColor
)),
maximumNumberOfLines: 1
))),
], alignment: .center, spacing: 2.0, fillWidth: true)),
leftIcon: nil,
icon: nil,
accessory: .none,
action: { [weak self] _ in
guard let self else {
return
}
self.peerList.categories.removeAll()
self.peerList.peers.removeAll()
self.state?.updated(transition: .spring(duration: 0.4))
if self.peerList.categories.isEmpty && self.peerList.peers.isEmpty {
let _ = self.attemptNavigation(complete: {})
self.environment?.controller()?.dismiss()
}
}
)))]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let clearSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: clearSectionSize)
if let clearSectionView = self.clearSection.view {
if clearSectionView.superview == nil {
self.scrollView.addSubview(clearSectionView)
}
transition.setFrame(view: clearSectionView, frame: clearSectionFrame)
alphaTransition.setAlpha(view: clearSectionView, alpha: (self.peerList.categories.isEmpty && self.peerList.peers.isEmpty) ? 0.0 : 1.0)
}
if !self.peerList.categories.isEmpty || !self.peerList.peers.isEmpty {
contentHeight += clearSectionSize.height
}
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.verticalScrollIndicatorInsets != scrollInsets {
self.scrollView.verticalScrollIndicatorInsets = scrollInsets
}
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
}
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class BusinessRecipientListScreen: ViewControllerComponentContainer {
final class InitialData {
fileprivate let peerList: BusinessRecipientListScreenComponent.PeerList
fileprivate init(
peerList: BusinessRecipientListScreenComponent.PeerList
) {
self.peerList = peerList
}
}
enum Mode {
case includeExceptions
case excludeExceptions
case excludeUsers
}
private let context: AccountContext
init(context: AccountContext, peerList: BusinessRecipientListScreenComponent.PeerList, mode: Mode, update: @escaping (BusinessRecipientListScreenComponent.PeerList) -> Void) {
self.context = context
super.init(context: context, component: BusinessRecipientListScreenComponent(
context: context,
initialData: InitialData(peerList: peerList),
mode: mode,
update: update
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.title = ""
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? BusinessRecipientListScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? BusinessRecipientListScreenComponent.View else {
return true
}
return componentView.attemptNavigation(complete: complete)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
}
@@ -0,0 +1,403 @@
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)?
var enumerateSiblings: (((UIView) -> Void) -> 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)
}
}