mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-01 04:08:07 +00:00
Business features
This commit is contained in:
parent
3d6c9b1745
commit
ac9c6a5f7f
@ -30,6 +30,7 @@ swift_library(
|
||||
"//submodules/Markdown",
|
||||
"//submodules/ReactionSelectionNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem",
|
||||
"//submodules/PremiumUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -21,6 +21,7 @@ import Markdown
|
||||
import ReactionSelectionNode
|
||||
import ChatMediaInputStickerGridItem
|
||||
import UndoUI
|
||||
import PremiumUI
|
||||
|
||||
private protocol ChatEmptyNodeContent {
|
||||
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize
|
||||
@ -1594,7 +1595,7 @@ private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode {
|
||||
self.badgeTextNode.attributedText = NSAttributedString(string: "how?", font: Font.regular(11.0), textColor: serviceColor.primaryText)
|
||||
let badgeTextSize = self.badgeTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
|
||||
if let lastLineFrame = labelRects.last {
|
||||
let badgeTextFrame = CGRect(origin: CGPoint(x: lastLineFrame.maxX - badgeTextSize.width - 2.0, y: textFrame.maxY - badgeTextSize.height), size: badgeTextSize)
|
||||
let badgeTextFrame = CGRect(origin: CGPoint(x: lastLineFrame.maxX - badgeTextSize.width - 3.0, y: textFrame.maxY - badgeTextSize.height), size: badgeTextSize)
|
||||
self.badgeTextNode.frame = badgeTextFrame
|
||||
|
||||
let badgeBackgroundFrame = badgeTextFrame.insetBy(dx: -4.0, dy: -1.0)
|
||||
@ -1857,6 +1858,7 @@ public final class ChatEmptyNode: ASDisplayNode {
|
||||
self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(20.0, self.backgroundNode.bounds.height / 2.0), transition: transition)
|
||||
|
||||
if displayAttachedDescription, let peer = interfaceState.renderedPeer?.chatMainPeer {
|
||||
let isPremium = interfaceState.isPremium
|
||||
let attachedDescriptionNode: EmptyAttachedDescriptionNode
|
||||
if let current = self.attachedDescriptionNode {
|
||||
attachedDescriptionNode = current
|
||||
@ -1869,7 +1871,32 @@ public final class ChatEmptyNode: ASDisplayNode {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .settings, forceDark: false, dismissed: nil)
|
||||
|
||||
//TODO:localize
|
||||
let context = self.context
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
var dismissImpl: (() -> Void)?
|
||||
let controller = PremiumLimitsListScreen(context: context, subject: .business, source: .other, order: [.business], buttonText: "OK", isPremium: false, forceDark: false)
|
||||
controller.action = {
|
||||
if isPremium {
|
||||
dismissImpl?()
|
||||
} else {
|
||||
let controller = PremiumIntroScreen(context: context, source: .settings, forceDark: false)
|
||||
replaceImpl?(controller)
|
||||
}
|
||||
}
|
||||
replaceImpl = { [weak self, weak controller] c in
|
||||
controller?.dismiss(animated: true, completion: {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.interaction?.chatController()?.push(c)
|
||||
})
|
||||
}
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss(animated: true, completion: {
|
||||
})
|
||||
}
|
||||
self.interaction?.chatController()?.push(controller)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1425,7 +1425,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
var version = 0
|
||||
strongSelf.stickerSearchDisposable.set((resultSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -272,7 +272,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
private let pagerView: ComponentHostView<EntityKeyboardChildEnvironment>
|
||||
|
||||
private var component: EntityKeyboardComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
public private(set) weak var state: EmptyComponentState?
|
||||
|
||||
private var searchView: ComponentHostView<EntitySearchContentEnvironment>?
|
||||
private var searchComponent: EntitySearchContentComponent?
|
||||
|
||||
@ -12,6 +12,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||
public final class ExternalState {
|
||||
public fileprivate(set) var hasText: Bool = false
|
||||
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
|
||||
public fileprivate(set) var isEditing: Bool = false
|
||||
|
||||
public init() {
|
||||
}
|
||||
@ -39,6 +40,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||
public let autocapitalizationType: UITextAutocapitalizationType
|
||||
public let autocorrectionType: UITextAutocorrectionType
|
||||
public let characterLimit: Int?
|
||||
public let displayCharacterLimit: Bool
|
||||
public let allowEmptyLines: Bool
|
||||
public let updated: ((String) -> Void)?
|
||||
public let textUpdateTransition: Transition
|
||||
@ -55,6 +57,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
||||
autocorrectionType: UITextAutocorrectionType = .default,
|
||||
characterLimit: Int? = nil,
|
||||
displayCharacterLimit: Bool = false,
|
||||
allowEmptyLines: Bool = true,
|
||||
updated: ((String) -> Void)?,
|
||||
textUpdateTransition: Transition = .immediate,
|
||||
@ -70,6 +73,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||
self.autocapitalizationType = autocapitalizationType
|
||||
self.autocorrectionType = autocorrectionType
|
||||
self.characterLimit = characterLimit
|
||||
self.displayCharacterLimit = displayCharacterLimit
|
||||
self.allowEmptyLines = allowEmptyLines
|
||||
self.updated = updated
|
||||
self.textUpdateTransition = textUpdateTransition
|
||||
@ -107,6 +111,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||
if lhs.characterLimit != rhs.characterLimit {
|
||||
return false
|
||||
}
|
||||
if lhs.displayCharacterLimit != rhs.displayCharacterLimit {
|
||||
return false
|
||||
}
|
||||
if lhs.allowEmptyLines != rhs.allowEmptyLines {
|
||||
return false
|
||||
}
|
||||
@ -134,6 +141,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||
|
||||
private let placeholder = ComponentView<Empty>()
|
||||
|
||||
private var measureTextLimitLabel: ComponentView<Empty>?
|
||||
private var textLimitLabel: ComponentView<Empty>?
|
||||
|
||||
private var component: ListMultilineTextFieldItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
@ -192,6 +202,29 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||
let verticalInset: CGFloat = 12.0
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
let textLimitFont = Font.regular(15.0)
|
||||
var measureTextLimitInset: CGFloat = 0.0
|
||||
if component.characterLimit != nil && component.displayCharacterLimit {
|
||||
let measureTextLimitLabel: ComponentView<Empty>
|
||||
if let current = self.measureTextLimitLabel {
|
||||
measureTextLimitLabel = current
|
||||
} else {
|
||||
measureTextLimitLabel = ComponentView()
|
||||
self.measureTextLimitLabel = measureTextLimitLabel
|
||||
}
|
||||
let measureTextLimitSize = measureTextLimitLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "000", font: textLimitFont))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
measureTextLimitInset = measureTextLimitSize.width + 4.0
|
||||
} else {
|
||||
self.measureTextLimitLabel = nil
|
||||
}
|
||||
|
||||
let textFieldSize = self.textField.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TextFieldComponent(
|
||||
@ -201,7 +234,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||
externalState: self.textFieldExternalState,
|
||||
fontSize: 17.0,
|
||||
textColor: component.theme.list.itemPrimaryTextColor,
|
||||
insets: UIEdgeInsets(top: verticalInset, left: sideInset - 8.0, bottom: verticalInset, right: sideInset - 8.0),
|
||||
insets: UIEdgeInsets(top: verticalInset, left: sideInset - 8.0, bottom: verticalInset, right: sideInset - 8.0 + measureTextLimitInset),
|
||||
hideKeyboard: false,
|
||||
customInputView: nil,
|
||||
resetText: component.resetText.flatMap { resetText in
|
||||
@ -258,6 +291,51 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||
|
||||
component.externalState?.hasText = self.textFieldExternalState.hasText
|
||||
component.externalState?.text = self.textFieldExternalState.text
|
||||
component.externalState?.isEditing = self.textFieldExternalState.isEditing
|
||||
|
||||
var displayRemainingLimit: Int?
|
||||
if let characterLimit = component.characterLimit, component.displayCharacterLimit {
|
||||
let remainingLimit = characterLimit - self.textFieldExternalState.text.length
|
||||
let displayThreshold = max(10, Int(Double(characterLimit) * 0.15))
|
||||
if remainingLimit <= displayThreshold {
|
||||
displayRemainingLimit = remainingLimit
|
||||
}
|
||||
}
|
||||
if let displayRemainingLimit {
|
||||
let textLimitLabel: ComponentView<Empty>
|
||||
var textLimitLabelTransition = transition
|
||||
if let current = self.textLimitLabel {
|
||||
textLimitLabel = current
|
||||
} else {
|
||||
textLimitLabelTransition = textLimitLabelTransition.withAnimation(.none)
|
||||
textLimitLabel = ComponentView()
|
||||
self.textLimitLabel = textLimitLabel
|
||||
}
|
||||
|
||||
let textLimitLabelSize = textLimitLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "\(displayRemainingLimit)", font: textLimitFont, textColor: component.theme.list.itemSecondaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
let textLimitLabelFrame = CGRect(origin: CGPoint(x: availableSize.width - textLimitLabelSize.width - sideInset, y: verticalInset + 2.0), size: textLimitLabelSize)
|
||||
if let textLimitLabelView = textLimitLabel.view {
|
||||
if textLimitLabelView.superview == nil {
|
||||
textLimitLabelView.isUserInteractionEnabled = false
|
||||
textLimitLabelView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.addSubview(textLimitLabelView)
|
||||
}
|
||||
textLimitLabelTransition.setPosition(view: textLimitLabelView, position: CGPoint(x: textLimitLabelFrame.maxX, y: textLimitLabelFrame.minY))
|
||||
textLimitLabelView.bounds = CGRect(origin: CGPoint(), size: textLimitLabelFrame.size)
|
||||
}
|
||||
} else {
|
||||
if let textLimitLabel = self.textLimitLabel {
|
||||
self.textLimitLabel = nil
|
||||
textLimitLabel.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
@ -17,7 +17,8 @@ public final class EmojiSelectionComponent: Component {
|
||||
public let sideInset: CGFloat
|
||||
public let bottomInset: CGFloat
|
||||
public let deviceMetrics: DeviceMetrics
|
||||
public let emojiContent: EmojiPagerContentComponent
|
||||
public let emojiContent: EmojiPagerContentComponent?
|
||||
public let stickerContent: EmojiPagerContentComponent?
|
||||
public let backgroundIconColor: UIColor?
|
||||
public let backgroundColor: UIColor
|
||||
public let separatorColor: UIColor
|
||||
@ -29,7 +30,8 @@ public final class EmojiSelectionComponent: Component {
|
||||
sideInset: CGFloat,
|
||||
bottomInset: CGFloat,
|
||||
deviceMetrics: DeviceMetrics,
|
||||
emojiContent: EmojiPagerContentComponent,
|
||||
emojiContent: EmojiPagerContentComponent?,
|
||||
stickerContent: EmojiPagerContentComponent?,
|
||||
backgroundIconColor: UIColor?,
|
||||
backgroundColor: UIColor,
|
||||
separatorColor: UIColor,
|
||||
@ -41,6 +43,7 @@ public final class EmojiSelectionComponent: Component {
|
||||
self.bottomInset = bottomInset
|
||||
self.deviceMetrics = deviceMetrics
|
||||
self.emojiContent = emojiContent
|
||||
self.stickerContent = stickerContent
|
||||
self.backgroundIconColor = backgroundIconColor
|
||||
self.backgroundColor = backgroundColor
|
||||
self.separatorColor = separatorColor
|
||||
@ -66,6 +69,9 @@ public final class EmojiSelectionComponent: Component {
|
||||
if lhs.emojiContent != rhs.emojiContent {
|
||||
return false
|
||||
}
|
||||
if lhs.stickerContent != rhs.stickerContent {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundIconColor != rhs.backgroundIconColor {
|
||||
return false
|
||||
}
|
||||
@ -96,6 +102,8 @@ public final class EmojiSelectionComponent: Component {
|
||||
private var component: EmojiSelectionComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var isSearchActive: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.keyboardView = ComponentView<Empty>()
|
||||
self.keyboardClippingView = UIView()
|
||||
@ -144,6 +152,12 @@ public final class EmojiSelectionComponent: Component {
|
||||
deinit {
|
||||
}
|
||||
|
||||
public func internalRequestUpdate(transition: Transition) {
|
||||
if let keyboardComponentView = self.keyboardView.view as? EntityKeyboardComponent.View {
|
||||
keyboardComponentView.state?.updated(transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.backgroundColor = component.backgroundColor
|
||||
let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85)
|
||||
@ -154,6 +168,11 @@ public final class EmojiSelectionComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
var resolvedHeight: CGFloat = min(340.0, max(50.0, availableSize.height - 200.0))
|
||||
if self.isSearchActive {
|
||||
resolvedHeight = min(availableSize.height, resolvedHeight + 200.0)
|
||||
}
|
||||
|
||||
self.cornersView.tintColor = component.theme.list.blocksBackgroundColor
|
||||
transition.setFrame(view: self.cornersView, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: availableSize.width, height: 16.0)))
|
||||
|
||||
@ -201,7 +220,7 @@ public final class EmojiSelectionComponent: Component {
|
||||
})
|
||||
}
|
||||
self.backspaceBackgroundView.frame = CGRect(origin: CGPoint(), size: backspaceButtonSize).insetBy(dx: -12.0, dy: -12.0)
|
||||
let backspaceButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - component.sideInset - backspaceButtonInset.right - backspaceButtonSize.width, y: availableSize.height - component.bottomInset - backspaceButtonInset.bottom), size: backspaceButtonSize)
|
||||
let backspaceButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - component.sideInset - backspaceButtonInset.right - backspaceButtonSize.width, y: resolvedHeight - component.bottomInset - backspaceButtonInset.bottom), size: backspaceButtonSize)
|
||||
|
||||
if let backspaceButtonView = self.backspaceButton.view {
|
||||
if backspaceButtonView.superview == nil {
|
||||
@ -220,6 +239,7 @@ public final class EmojiSelectionComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
self.keyboardView.parentState = state
|
||||
let keyboardSize = self.keyboardView.update(
|
||||
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
|
||||
component: AnyComponent(EntityKeyboardComponent(
|
||||
@ -228,8 +248,8 @@ public final class EmojiSelectionComponent: Component {
|
||||
isContentInFocus: true,
|
||||
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: component.sideInset, bottom: component.bottomInset + 16.0, right: component.sideInset),
|
||||
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
|
||||
emojiContent: component.emojiContent.withCustomTintColor(component.theme.list.itemPrimaryTextColor),
|
||||
stickerContent: nil,
|
||||
emojiContent: component.emojiContent?.withCustomTintColor(component.theme.list.itemPrimaryTextColor),
|
||||
stickerContent: component.stickerContent?.withCustomTintColor(component.theme.list.itemPrimaryTextColor),
|
||||
maskContent: nil,
|
||||
gifContent: nil,
|
||||
hasRecentGifs: false,
|
||||
@ -241,7 +261,15 @@ public final class EmojiSelectionComponent: Component {
|
||||
topPanelExtensionUpdated: { _, _ in },
|
||||
topPanelScrollingOffset: { _, _ in },
|
||||
hideInputUpdated: { _, _, _ in },
|
||||
hideTopPanelUpdated: { _, _ in },
|
||||
hideTopPanelUpdated: { [weak self] hideTopPanel, transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.isSearchActive != hideTopPanel {
|
||||
self.isSearchActive = hideTopPanel
|
||||
self.state?.updated(transition: transition)
|
||||
}
|
||||
},
|
||||
switchToTextInput: {},
|
||||
switchToGifSubject: { _ in },
|
||||
reorderItems: { _, _ in },
|
||||
@ -257,7 +285,7 @@ public final class EmojiSelectionComponent: Component {
|
||||
customTintColor: component.backgroundIconColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
containerSize: CGSize(width: availableSize.width, height: resolvedHeight)
|
||||
)
|
||||
if let keyboardComponentView = self.keyboardView.view {
|
||||
if keyboardComponentView.superview == nil {
|
||||
@ -270,7 +298,7 @@ public final class EmojiSelectionComponent: Component {
|
||||
self.keyboardClippingView.clipsToBounds = false
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight)))
|
||||
transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: resolvedHeight - topPanelHeight)))
|
||||
|
||||
transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize))
|
||||
transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0)))
|
||||
@ -282,7 +310,7 @@ public final class EmojiSelectionComponent: Component {
|
||||
transition.setAlpha(view: self.panelSeparatorView, alpha: 1.0)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
return CGSize(width: availableSize.width, height: resolvedHeight)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -884,6 +884,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
bottomInset: environment.safeInsets.bottom,
|
||||
deviceMetrics: environment.deviceMetrics,
|
||||
emojiContent: emojiContent.withSelectedItems(Set(enabledReactions.map(\.file.fileId))),
|
||||
stickerContent: nil,
|
||||
backgroundIconColor: nil,
|
||||
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||
separatorColor: environment.theme.list.itemBlocksSeparatorColor,
|
||||
|
||||
@ -51,6 +51,23 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmojiSearchResult {
|
||||
var groups: [EmojiPagerContentComponent.ItemGroup]
|
||||
var id: AnyHashable
|
||||
var version: Int
|
||||
var isPreset: Bool
|
||||
}
|
||||
|
||||
private struct EmojiSearchState {
|
||||
var result: EmojiSearchResult?
|
||||
var isSearching: Bool
|
||||
|
||||
init(result: EmojiSearchResult?, isSearching: Bool) {
|
||||
self.result = result
|
||||
self.isSearching = isSearching
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let topOverscrollLayer = SimpleLayer()
|
||||
private let scrollView: ScrollView
|
||||
@ -74,12 +91,18 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
private let textInputTag = NSObject()
|
||||
private var resetText: String?
|
||||
|
||||
private var previousHadInputHeight: Bool = false
|
||||
private var recenterOnTag: NSObject?
|
||||
|
||||
private var stickerFile: TelegramMediaFile?
|
||||
|
||||
private var stickerContent: EmojiPagerContentComponent?
|
||||
private var stickerContentDisposable: Disposable?
|
||||
private let stickerSearchDisposable = MetaDisposable()
|
||||
private var stickerSearchState = EmojiSearchState(result: nil, isSearching: false)
|
||||
|
||||
private var displayStickerInput: Bool = false
|
||||
private var stickerSelectionControlDimView: UIView?
|
||||
private var stickerSelectionControl: ComponentView<Empty>?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@ -171,6 +194,13 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func stickerSelectionControlDimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.displayStickerInput = false
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: BusinessIntroSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -193,9 +223,10 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
|
||||
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
|
||||
chatPeerId: nil,
|
||||
hasSearch: false,
|
||||
hasSearch: true,
|
||||
hasTrending: false,
|
||||
forceHasPremium: true
|
||||
forceHasPremium: true,
|
||||
searchIsPlaceholderOnly: false
|
||||
)
|
||||
self.stickerContentDisposable = (stickerContent
|
||||
|> deliverOnMainQueue).start(next: { [weak self] stickerContent in
|
||||
@ -216,16 +247,16 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
self.stickerFile = itemFile
|
||||
self.displayStickerInput = false
|
||||
|
||||
self.stickerSearchDisposable.set(nil)
|
||||
self.stickerSearchState = EmojiSearchState(result: nil, isSearching: false)
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.25))
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
deleteBackwards: {
|
||||
},
|
||||
openStickerSettings: {
|
||||
},
|
||||
openFeatured: {
|
||||
},
|
||||
deleteBackwards: nil,
|
||||
openStickerSettings: nil,
|
||||
openFeatured: nil,
|
||||
openSearch: {
|
||||
},
|
||||
addGroupAction: { _, _, _ in
|
||||
@ -243,9 +274,202 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
navigationController: {
|
||||
return nil
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let stickerSelectionControlView = self.stickerSelectionControl?.view as? EmojiSelectionComponent.View {
|
||||
stickerSelectionControlView.internalRequestUpdate(transition: transition)
|
||||
}
|
||||
},
|
||||
updateSearchQuery: { _ in
|
||||
updateSearchQuery: { [weak self] query in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
switch query {
|
||||
case .none:
|
||||
self.stickerSearchDisposable.set(nil)
|
||||
self.stickerSearchState = EmojiSearchState(result: nil, isSearching: false)
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
case let .text(rawQuery, _):
|
||||
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if query.isEmpty {
|
||||
self.stickerSearchDisposable.set(nil)
|
||||
self.stickerSearchState = EmojiSearchState(result: nil, isSearching: false)
|
||||
self.state?.updated(transition: .immediate)
|
||||
} else {
|
||||
let context = component.context
|
||||
|
||||
let localSets = context.engine.stickers.searchStickerSets(query: query)
|
||||
let remoteSets: Signal<FoundStickerSets?, NoError> = .single(nil) |> then(
|
||||
context.engine.stickers.searchStickerSetsRemotely(query: query)
|
||||
|> map(Optional.init)
|
||||
)
|
||||
|
||||
let resultSignal = combineLatest(
|
||||
localSets,
|
||||
remoteSets
|
||||
)
|
||||
|> mapToSignal { localSets, remoteSets -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
if localSets.infos.isEmpty && remoteSets == nil {
|
||||
return .complete()
|
||||
}
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var mergedSets = localSets
|
||||
if let remoteSets {
|
||||
mergedSets = mergedSets.merge(with: remoteSets)
|
||||
}
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for entry in mergedSets.entries {
|
||||
guard let stickerPackItem = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
let itemFile = stickerPackItem.file
|
||||
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return .single([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
badge: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
hasEdit: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
)])
|
||||
}
|
||||
|
||||
var version = 0
|
||||
self.stickerSearchState.isSearching = true
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
self.stickerSearchDisposable.set((resultSignal
|
||||
|> delay(0.15, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.stickerSearchState = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false)
|
||||
version += 1
|
||||
self.state?.updated(transition: .immediate)
|
||||
}))
|
||||
}
|
||||
case let .category(value):
|
||||
let resultSignal = component.context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote])
|
||||
|> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for item in files.items {
|
||||
let itemFile = item.file
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: itemFile.isPremiumSticker ? .premium : .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return .single(([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
badge: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
hasEdit: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
)], files.isFinalResult))
|
||||
}
|
||||
|
||||
var version = 0
|
||||
self.stickerSearchDisposable.set((resultSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let group = result.items.first else {
|
||||
return
|
||||
}
|
||||
if group.items.isEmpty && !result.isFinalResult {
|
||||
self.stickerSearchState = EmojiSearchState(result: EmojiSearchResult(groups: [
|
||||
EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
badge: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
hasEdit: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: true,
|
||||
items: []
|
||||
)
|
||||
], id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.stickerSearchState = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
|
||||
version += 1
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}))
|
||||
}
|
||||
},
|
||||
updateScrollingToItemGroup: {
|
||||
},
|
||||
@ -349,6 +573,7 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
characterLimit: 32,
|
||||
displayCharacterLimit: true,
|
||||
allowEmptyLines: false,
|
||||
updated: { _ in
|
||||
},
|
||||
@ -369,6 +594,7 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
characterLimit: 70,
|
||||
displayCharacterLimit: true,
|
||||
allowEmptyLines: false,
|
||||
updated: { _ in
|
||||
},
|
||||
@ -416,6 +642,8 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
self.displayStickerInput = true
|
||||
self.endEditing(true)
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.5))
|
||||
}
|
||||
@ -494,6 +722,15 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize))
|
||||
}
|
||||
|
||||
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) {
|
||||
if self.titleInputState.isEditing {
|
||||
self.recenterOnTag = self.titleInputTag
|
||||
} else if self.textInputState.isEditing {
|
||||
self.recenterOnTag = self.textInputTag
|
||||
}
|
||||
}
|
||||
self.previousHadInputHeight = environment.inputHeight > 0.0
|
||||
|
||||
let displayDelete = !self.titleInputState.text.string.isEmpty || !self.textInputState.text.string.isEmpty || self.stickerFile != nil
|
||||
|
||||
var deleteSectionHeight: CGFloat = 0.0
|
||||
@ -557,6 +794,16 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
|
||||
var inputHeight: CGFloat = environment.inputHeight
|
||||
if self.displayStickerInput, let stickerContent = self.stickerContent {
|
||||
let stickerSelectionControlDimView: UIView
|
||||
if let current = self.stickerSelectionControlDimView {
|
||||
stickerSelectionControlDimView = current
|
||||
} else {
|
||||
stickerSelectionControlDimView = UIView()
|
||||
self.stickerSelectionControlDimView = stickerSelectionControlDimView
|
||||
self.addSubview(stickerSelectionControlDimView)
|
||||
stickerSelectionControlDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.stickerSelectionControlDimTapGesture(_:))))
|
||||
}
|
||||
|
||||
let stickerSelectionControl: ComponentView<Empty>
|
||||
var animateIn = false
|
||||
if let current = self.stickerSelectionControl {
|
||||
@ -570,22 +817,45 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
if let stickerFile = self.stickerFile {
|
||||
selectedItems.insert(stickerFile.fileId)
|
||||
}
|
||||
stickerSelectionControl.parentState = state
|
||||
|
||||
var stickerContent = stickerContent
|
||||
|
||||
if let stickerSearchResult = self.stickerSearchState.result {
|
||||
var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults?
|
||||
if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
|
||||
stickerSearchResults = EmojiPagerContentComponent.EmptySearchResults(
|
||||
text: environment.strings.EmojiSearch_SearchStickersEmptyResult,
|
||||
iconFile: nil
|
||||
)
|
||||
}
|
||||
let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true)
|
||||
stickerContent = stickerContent.withUpdatedItemGroups(panelItemGroups: stickerContent.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: self.stickerSearchState.isSearching ? .searching : defaultSearchState)
|
||||
} else if self.stickerSearchState.isSearching {
|
||||
stickerContent = stickerContent.withUpdatedItemGroups(panelItemGroups: stickerContent.panelItemGroups, contentItemGroups: stickerContent.contentItemGroups, itemContentUniqueId: stickerContent.itemContentUniqueId, emptySearchResults: stickerContent.emptySearchResults, searchState: .searching)
|
||||
}
|
||||
|
||||
let stickerSelectionControlTransition = animateIn ? .immediate : transition
|
||||
|
||||
stickerSelectionControlTransition.setFrame(view: stickerSelectionControlDimView, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight), size: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight)))
|
||||
|
||||
let stickerSelectionControlSize = stickerSelectionControl.update(
|
||||
transition: animateIn ? .immediate : transition,
|
||||
transition: stickerSelectionControlTransition,
|
||||
component: AnyComponent(EmojiSelectionComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
sideInset: environment.safeInsets.left,
|
||||
bottomInset: environment.safeInsets.bottom,
|
||||
deviceMetrics: environment.deviceMetrics,
|
||||
emojiContent: stickerContent.withSelectedItems(selectedItems),
|
||||
emojiContent: nil,
|
||||
stickerContent: stickerContent.withSelectedItems(selectedItems),
|
||||
backgroundIconColor: nil,
|
||||
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||
separatorColor: environment.theme.list.itemBlocksSeparatorColor,
|
||||
backspace: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: min(340.0, max(50.0, availableSize.height - 200.0)))
|
||||
containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight)
|
||||
)
|
||||
let stickerSelectionControlFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - stickerSelectionControlSize.height), size: stickerSelectionControlSize)
|
||||
if let stickerSelectionControlView = stickerSelectionControl.view {
|
||||
@ -600,12 +870,18 @@ final class BusinessIntroSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
inputHeight = stickerSelectionControlSize.height
|
||||
} else if let stickerSelectionControl = self.stickerSelectionControl {
|
||||
self.stickerSelectionControl = nil
|
||||
if let stickerSelectionControlView = stickerSelectionControl.view {
|
||||
transition.setPosition(view: stickerSelectionControlView, position: CGPoint(x: stickerSelectionControlView.center.x, y: availableSize.height + stickerSelectionControlView.bounds.height * 0.5), completion: { [weak stickerSelectionControlView] _ in
|
||||
stickerSelectionControlView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
if let stickerSelectionControl = self.stickerSelectionControl {
|
||||
self.stickerSelectionControl = nil
|
||||
if let stickerSelectionControlView = stickerSelectionControl.view {
|
||||
transition.setPosition(view: stickerSelectionControlView, position: CGPoint(x: stickerSelectionControlView.center.x, y: availableSize.height + stickerSelectionControlView.bounds.height * 0.5), completion: { [weak stickerSelectionControlView] _ in
|
||||
stickerSelectionControlView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
if let stickerSelectionControlDimView = self.stickerSelectionControlDimView {
|
||||
self.stickerSelectionControlDimView = nil
|
||||
stickerSelectionControlDimView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -257,9 +257,15 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
if !query.isEmpty {
|
||||
if self.botResolutionState?.query != query {
|
||||
let previousState = self.botResolutionState?.state
|
||||
let updatedState: BotResolutionState.State
|
||||
if let current = self.botResolutionState?.state, case .found = current {
|
||||
updatedState = current
|
||||
} else {
|
||||
updatedState = .searching
|
||||
}
|
||||
self.botResolutionState = BotResolutionState(
|
||||
query: query,
|
||||
state: self.botResolutionState?.state ?? .searching
|
||||
state: updatedState
|
||||
)
|
||||
self.botResolutionDisposable?.dispose()
|
||||
|
||||
@ -267,7 +273,19 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
self.state?.updated(transition: .spring(duration: 0.35))
|
||||
}
|
||||
|
||||
self.botResolutionDisposable = (component.context.engine.peers.resolvePeerByName(name: query)
|
||||
var cleanQuery = query
|
||||
if let url = URL(string: cleanQuery), url.host == "t.me" {
|
||||
if url.pathComponents.count > 1 {
|
||||
cleanQuery = url.pathComponents[1]
|
||||
}
|
||||
} else if let url = URL(string: "https://\(cleanQuery)"), url.host == "t.me" {
|
||||
if url.pathComponents.count > 1 {
|
||||
cleanQuery = url.pathComponents[1]
|
||||
}
|
||||
}
|
||||
|
||||
self.botResolutionDisposable = (component.context.engine.peers.resolvePeerByName(name: cleanQuery)
|
||||
|> delay(0.4, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
@ -661,10 +679,20 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.endEditing(true)
|
||||
|
||||
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))
|
||||
if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.isBusiness) {
|
||||
botResolutionState.state = .found(peer: peer, isInstalled: true)
|
||||
self.botResolutionState = botResolutionState
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
} else {
|
||||
//TODO:localize
|
||||
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "This bot doesn't support Telegram Business yet.", actions: [
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
||||
})
|
||||
]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
},
|
||||
removeAction: { [weak self] in
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user