[WIP] Business

This commit is contained in:
Isaac
2024-02-13 20:13:40 +04:00
parent 71a40dcdb2
commit d9fec0a500
49 changed files with 5678 additions and 128 deletions

View File

@@ -31,6 +31,10 @@ swift_library(
"//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",

View File

@@ -0,0 +1,405 @@
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: Transition) -> 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
}
//TODO:localize
addButtonSize = addButton.update(
transition: addButtonTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "ADD", 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: 100.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)
Transition.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
}
//TODO:localize
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)
Transition.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 = "bot" //TODO:localize
case let .found(peer, _):
isTextVisible = true
titleValue = peer.displayTitle(strings: component.strings, displayOrder: .firstLast)
subtitleValue = "bot"
}
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
}
//TODO:localize
let notFoundLabelSize = notFoundLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Chatbot not found", 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)
Transition.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)
Transition.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: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@@ -21,6 +21,8 @@ import ListTextFieldItemComponent
import BundleIconComponent
import LottieComponent
import Markdown
import PeerListItemComponent
import AvatarNode
private let checkIcon: UIImage = {
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in
@@ -60,6 +62,49 @@ final class ChatbotSetupScreenComponent: Component {
}
}
private struct BotResolutionState: Equatable {
enum State: Equatable {
case searching
case notFound
case found(peer: EnginePeer, isInstalled: Bool)
}
var query: String
var state: State
init(query: String, state: State) {
self.query = query
self.state = state
}
}
private struct AdditionalPeerList {
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
@@ -79,6 +124,18 @@ final class ChatbotSetupScreenComponent: Component {
private var environment: EnvironmentType?
private var chevronImage: UIImage?
private let textFieldTag = NSObject()
private var botResolutionState: BotResolutionState?
private var botResolutionDisposable: Disposable?
private var hasAccessToAllChatsByDefault: Bool = true
private var additionalPeerList = AdditionalPeerList(
categories: Set(),
peers: []
)
private var replyToMessages: Bool = true
override init(frame: CGRect) {
self.scrollView = ScrollView()
@@ -150,6 +207,184 @@ final class ChatbotSetupScreenComponent: Component {
}
}
private func updateBotQuery(query: String) {
guard let component = self.component else {
return
}
if !query.isEmpty {
if self.botResolutionState?.query != query {
let previousState = self.botResolutionState?.state
self.botResolutionState = BotResolutionState(
query: query,
state: self.botResolutionState?.state ?? .searching
)
self.botResolutionDisposable?.dispose()
if previousState != self.botResolutionState?.state {
self.state?.updated(transition: .spring(duration: 0.35))
}
self.botResolutionDisposable = (component.context.engine.peers.resolvePeerByName(name: query)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
switch result {
case .progress:
break
case let .result(peer):
let previousState = self.botResolutionState?.state
if let peer {
self.botResolutionState?.state = .found(peer: peer, isInstalled: false)
} else {
self.botResolutionState?.state = .notFound
}
if previousState != self.botResolutionState?.state {
self.state?.updated(transition: .spring(duration: 0.35))
}
}
})
}
} else {
if let botResolutionDisposable = self.botResolutionDisposable {
self.botResolutionDisposable = nil
botResolutionDisposable.dispose()
}
if self.botResolutionState != nil {
self.botResolutionState = nil
self.state?.updated(transition: .spring(duration: 0.35))
}
}
}
private func openAdditionalPeerListSetup() {
guard let component = self.component else {
return
}
enum AdditionalCategoryId: Int {
case existingChats
case newChats
case contacts
case nonContacts
}
let additionalCategories: [ChatListNodeAdditionalCategory] = [
ChatListNodeAdditionalCategory(
id: self.hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.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: .purple),
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: .purple),
title: self.hasAccessToAllChatsByDefault ? "Existing Chats" : "New Chats"
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.contacts.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: .blue),
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: .blue),
title: "Contacts"
),
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: "Non-Contacts"
)
]
var selectedCategories = Set<Int>()
for category in self.additionalPeerList.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)
}
}
//TODO:localize
let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: self.hasAccessToAllChatsByDefault ? "Exclude Chats" : "Include Chats",
searchPlaceholder: "Search chats",
selectedChats: Set(self.additionalPeerList.peers.map(\.peer.id)),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
chatListFilters: nil,
onlyUsers: true
)), options: [], filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in
}))
controller.navigationPresentation = .modal
let _ = (controller.result
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in
guard let self, let component = self.component, 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 _ = (component.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: { [weak self] peerMap, isContactMap in
guard let self else {
return
}
let mappedCategories = additionalCategoryIds.compactMap { item -> AdditionalPeerList.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
}
}
self.additionalPeerList.categories = Set(mappedCategories)
self.additionalPeerList.peers.removeAll()
for id in peerIds {
guard let maybePeer = peerMap[id], let peer = maybePeer else {
continue
}
self.additionalPeerList.peers.append(AdditionalPeerList.Peer(
peer: peer,
isContact: isContactMap[id] ?? false
))
}
self.additionalPeerList.peers.sort(by: { lhs, rhs in
return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle
})
self.state?.updated(transition: .immediate)
controller?.dismiss()
})
})
self.environment?.controller()?.push(controller)
}
func update(component: ChatbotSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
@@ -221,7 +456,7 @@ final class ChatbotSetupScreenComponent: Component {
contentHeight += 129.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More>]()", attributes: MarkdownAttributes(
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More]()", attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
@@ -239,7 +474,7 @@ final class ChatbotSetupScreenComponent: Component {
//TODO:localize
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
component: AnyComponent(BalancedTextComponent(
text: .plain(subtitleString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
@@ -273,6 +508,66 @@ final class ChatbotSetupScreenComponent: Component {
contentHeight += subtitleSize.height
contentHeight += 27.0
var nameSectionItems: [AnyComponentWithIdentity<Empty>] = []
nameSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent(
theme: environment.theme,
initialText: "",
placeholder: "Bot Username",
autocapitalizationType: .none,
autocorrectionType: .no,
updated: { [weak self] value in
guard let self else {
return
}
self.updateBotQuery(query: value)
},
tag: self.textFieldTag
))))
if let botResolutionState = self.botResolutionState {
let mappedContent: ChatbotSearchResultItemComponent.Content
switch botResolutionState.state {
case .searching:
mappedContent = .searching
case .notFound:
mappedContent = .notFound
case let .found(peer, isInstalled):
mappedContent = .found(peer: peer, isInstalled: isInstalled)
}
nameSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ChatbotSearchResultItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
content: mappedContent,
installAction: { [weak self] in
guard let self else {
return
}
if var botResolutionState = self.botResolutionState, case let .found(peer, isInstalled) = botResolutionState.state, !isInstalled {
botResolutionState.state = .found(peer: peer, isInstalled: true)
self.botResolutionState = botResolutionState
self.state?.updated(transition: .spring(duration: 0.3))
}
},
removeAction: { [weak self] in
guard let self else {
return
}
if let botResolutionState = self.botResolutionState, case let .found(_, isInstalled) = botResolutionState.state, isInstalled {
self.botResolutionState = nil
if let botResolutionDisposable = self.botResolutionDisposable {
self.botResolutionDisposable = nil
botResolutionDisposable.dispose()
}
if let textFieldView = self.nameSection.findTaggedView(tag: self.textFieldTag) as? ListTextFieldItemComponent.View {
textFieldView.setText(text: "", updateState: false)
}
self.state?.updated(transition: .spring(duration: 0.3))
}
}
))))
}
//TODO:localize
let nameSectionSize = self.nameSection.update(
transition: transition,
@@ -287,15 +582,7 @@ final class ChatbotSetupScreenComponent: Component {
)),
maximumNumberOfLines: 0
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent(
theme: environment.theme,
initialText: "",
placeholder: "Bot Username",
updated: { value in
}
)))
]
items: nameSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
@@ -339,11 +626,20 @@ final class ChatbotSetupScreenComponent: Component {
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon,
tintColor: environment.theme.list.itemAccentColor,
tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center
))),
accessory: nil,
action: { _ in
action: { [weak self] _ in
guard let self else {
return
}
if !self.hasAccessToAllChatsByDefault {
self.hasAccessToAllChatsByDefault = true
self.additionalPeerList.categories.removeAll()
self.additionalPeerList.peers.removeAll()
self.state?.updated(transition: .immediate)
}
}
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent(
@@ -360,11 +656,20 @@ final class ChatbotSetupScreenComponent: Component {
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon,
tintColor: .clear,
tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center
))),
accessory: nil,
action: { _ in
action: { [weak self] _ in
guard let self else {
return
}
if self.hasAccessToAllChatsByDefault {
self.hasAccessToAllChatsByDefault = false
self.additionalPeerList.categories.removeAll()
self.additionalPeerList.peers.removeAll()
self.state?.updated(transition: .immediate)
}
}
)))
]
@@ -382,6 +687,95 @@ final class ChatbotSetupScreenComponent: Component {
contentHeight += accessSectionSize.height
contentHeight += sectionSpacing
var excludedSectionItems: [AnyComponentWithIdentity<Empty>] = []
excludedSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: self.hasAccessToAllChatsByDefault ? "Exclude Chats..." : "Select Chats...",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
name: "Chat List/AddIcon",
tintColor: environment.theme.list.itemAccentColor
))),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openAdditionalPeerListSetup()
}
))))
for category in self.additionalPeerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) {
let title: String
let icon: String
let color: AvatarBackgroundColor
//TODO:localize
switch category {
case .newChats:
title = "New Chats"
icon = "Chat List/Filters/Contact"
color = .purple
case .existingChats:
title = "Existing Chats"
icon = "Chat List/Filters/Contact"
color = .purple
case .contacts:
title = "Contacts"
icon = "Chat List/Filters/Contact"
color = .blue
case .nonContacts:
title = "Non-Contacts"
icon = "Chat List/Filters/Contact"
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
}
))))
}
for peer in self.additionalPeerList.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: peer.isContact ? "contact" : "non-contact",
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: false,
action: { peer, _, _ in
}
))))
}
//TODO:localize
let excludedSectionSize = self.excludedSection.update(
transition: transition,
@@ -389,42 +783,27 @@ final class ChatbotSetupScreenComponent: Component {
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "EXCLUDED CHATS",
string: self.hasAccessToAllChatsByDefault ? "EXCLUDED CHATS" : "INCLUDED CHATS",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Select chats or entire chat categories which the bot WILL NOT have access to.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
text: .markdown(
text: self.hasAccessToAllChatsByDefault ? "Select chats or entire chat categories which the bot **WILL NOT** have access to." : "Select chats or entire chat categories which the bot **WILL** have access to.",
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { _ in
return nil
}
)
),
maximumNumberOfLines: 0
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Exclude Chats...",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
name: "Chat List/AddIcon",
tintColor: environment.theme.list.itemAccentColor
))),
accessory: nil,
action: { _ in
}
))),
]
items: excludedSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
@@ -473,7 +852,13 @@ final class ChatbotSetupScreenComponent: Component {
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(true),
accessory: .toggle(ListActionItemComponent.Toggle(style: .icons, isOn: self.replyToMessages, action: { [weak self] _ in
guard let self else {
return
}
self.replyToMessages = !self.replyToMessages
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))),
]