mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
[WIP] Business
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
))),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user