Business features

This commit is contained in:
Isaac 2024-03-26 01:32:26 +04:00
parent 3d6c9b1745
commit ac9c6a5f7f
9 changed files with 478 additions and 39 deletions

View File

@ -30,6 +30,7 @@ swift_library(
"//submodules/Markdown", "//submodules/Markdown",
"//submodules/ReactionSelectionNode", "//submodules/ReactionSelectionNode",
"//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem", "//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem",
"//submodules/PremiumUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -21,6 +21,7 @@ import Markdown
import ReactionSelectionNode import ReactionSelectionNode
import ChatMediaInputStickerGridItem import ChatMediaInputStickerGridItem
import UndoUI import UndoUI
import PremiumUI
private protocol ChatEmptyNodeContent { private protocol ChatEmptyNodeContent {
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize 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) 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)) let badgeTextSize = self.badgeTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
if let lastLineFrame = labelRects.last { 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 self.badgeTextNode.frame = badgeTextFrame
let badgeBackgroundFrame = badgeTextFrame.insetBy(dx: -4.0, dy: -1.0) 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) 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 { if displayAttachedDescription, let peer = interfaceState.renderedPeer?.chatMainPeer {
let isPremium = interfaceState.isPremium
let attachedDescriptionNode: EmptyAttachedDescriptionNode let attachedDescriptionNode: EmptyAttachedDescriptionNode
if let current = self.attachedDescriptionNode { if let current = self.attachedDescriptionNode {
attachedDescriptionNode = current attachedDescriptionNode = current
@ -1869,7 +1871,32 @@ public final class ChatEmptyNode: ASDisplayNode {
guard let self else { guard let self else {
return 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) self.interaction?.chatController()?.push(controller)
} }
} }

View File

@ -1425,7 +1425,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
var version = 0 var version = 0
strongSelf.stickerSearchDisposable.set((resultSignal strongSelf.stickerSearchDisposable.set((resultSignal
|> deliverOnMainQueue).start(next: { [weak self] result in |> deliverOnMainQueue).start(next: { result in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }

View File

@ -272,7 +272,7 @@ public final class EntityKeyboardComponent: Component {
private let pagerView: ComponentHostView<EntityKeyboardChildEnvironment> private let pagerView: ComponentHostView<EntityKeyboardChildEnvironment>
private var component: EntityKeyboardComponent? private var component: EntityKeyboardComponent?
private weak var state: EmptyComponentState? public private(set) weak var state: EmptyComponentState?
private var searchView: ComponentHostView<EntitySearchContentEnvironment>? private var searchView: ComponentHostView<EntitySearchContentEnvironment>?
private var searchComponent: EntitySearchContentComponent? private var searchComponent: EntitySearchContentComponent?

View File

@ -12,6 +12,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
public final class ExternalState { public final class ExternalState {
public fileprivate(set) var hasText: Bool = false public fileprivate(set) var hasText: Bool = false
public fileprivate(set) var text: NSAttributedString = NSAttributedString() public fileprivate(set) var text: NSAttributedString = NSAttributedString()
public fileprivate(set) var isEditing: Bool = false
public init() { public init() {
} }
@ -39,6 +40,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
public let autocapitalizationType: UITextAutocapitalizationType public let autocapitalizationType: UITextAutocapitalizationType
public let autocorrectionType: UITextAutocorrectionType public let autocorrectionType: UITextAutocorrectionType
public let characterLimit: Int? public let characterLimit: Int?
public let displayCharacterLimit: Bool
public let allowEmptyLines: Bool public let allowEmptyLines: Bool
public let updated: ((String) -> Void)? public let updated: ((String) -> Void)?
public let textUpdateTransition: Transition public let textUpdateTransition: Transition
@ -55,6 +57,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
autocapitalizationType: UITextAutocapitalizationType = .sentences, autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default, autocorrectionType: UITextAutocorrectionType = .default,
characterLimit: Int? = nil, characterLimit: Int? = nil,
displayCharacterLimit: Bool = false,
allowEmptyLines: Bool = true, allowEmptyLines: Bool = true,
updated: ((String) -> Void)?, updated: ((String) -> Void)?,
textUpdateTransition: Transition = .immediate, textUpdateTransition: Transition = .immediate,
@ -70,6 +73,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
self.autocapitalizationType = autocapitalizationType self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType self.autocorrectionType = autocorrectionType
self.characterLimit = characterLimit self.characterLimit = characterLimit
self.displayCharacterLimit = displayCharacterLimit
self.allowEmptyLines = allowEmptyLines self.allowEmptyLines = allowEmptyLines
self.updated = updated self.updated = updated
self.textUpdateTransition = textUpdateTransition self.textUpdateTransition = textUpdateTransition
@ -107,6 +111,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
if lhs.characterLimit != rhs.characterLimit { if lhs.characterLimit != rhs.characterLimit {
return false return false
} }
if lhs.displayCharacterLimit != rhs.displayCharacterLimit {
return false
}
if lhs.allowEmptyLines != rhs.allowEmptyLines { if lhs.allowEmptyLines != rhs.allowEmptyLines {
return false return false
} }
@ -134,6 +141,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
private let placeholder = ComponentView<Empty>() private let placeholder = ComponentView<Empty>()
private var measureTextLimitLabel: ComponentView<Empty>?
private var textLimitLabel: ComponentView<Empty>?
private var component: ListMultilineTextFieldItemComponent? private var component: ListMultilineTextFieldItemComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
private var isUpdating: Bool = false private var isUpdating: Bool = false
@ -192,6 +202,29 @@ public final class ListMultilineTextFieldItemComponent: Component {
let verticalInset: CGFloat = 12.0 let verticalInset: CGFloat = 12.0
let sideInset: CGFloat = 16.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( let textFieldSize = self.textField.update(
transition: transition, transition: transition,
component: AnyComponent(TextFieldComponent( component: AnyComponent(TextFieldComponent(
@ -201,7 +234,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
externalState: self.textFieldExternalState, externalState: self.textFieldExternalState,
fontSize: 17.0, fontSize: 17.0,
textColor: component.theme.list.itemPrimaryTextColor, 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, hideKeyboard: false,
customInputView: nil, customInputView: nil,
resetText: component.resetText.flatMap { resetText in resetText: component.resetText.flatMap { resetText in
@ -258,6 +291,51 @@ public final class ListMultilineTextFieldItemComponent: Component {
component.externalState?.hasText = self.textFieldExternalState.hasText component.externalState?.hasText = self.textFieldExternalState.hasText
component.externalState?.text = self.textFieldExternalState.text 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 return size
} }

View File

@ -17,7 +17,8 @@ public final class EmojiSelectionComponent: Component {
public let sideInset: CGFloat public let sideInset: CGFloat
public let bottomInset: CGFloat public let bottomInset: CGFloat
public let deviceMetrics: DeviceMetrics public let deviceMetrics: DeviceMetrics
public let emojiContent: EmojiPagerContentComponent public let emojiContent: EmojiPagerContentComponent?
public let stickerContent: EmojiPagerContentComponent?
public let backgroundIconColor: UIColor? public let backgroundIconColor: UIColor?
public let backgroundColor: UIColor public let backgroundColor: UIColor
public let separatorColor: UIColor public let separatorColor: UIColor
@ -29,7 +30,8 @@ public final class EmojiSelectionComponent: Component {
sideInset: CGFloat, sideInset: CGFloat,
bottomInset: CGFloat, bottomInset: CGFloat,
deviceMetrics: DeviceMetrics, deviceMetrics: DeviceMetrics,
emojiContent: EmojiPagerContentComponent, emojiContent: EmojiPagerContentComponent?,
stickerContent: EmojiPagerContentComponent?,
backgroundIconColor: UIColor?, backgroundIconColor: UIColor?,
backgroundColor: UIColor, backgroundColor: UIColor,
separatorColor: UIColor, separatorColor: UIColor,
@ -41,6 +43,7 @@ public final class EmojiSelectionComponent: Component {
self.bottomInset = bottomInset self.bottomInset = bottomInset
self.deviceMetrics = deviceMetrics self.deviceMetrics = deviceMetrics
self.emojiContent = emojiContent self.emojiContent = emojiContent
self.stickerContent = stickerContent
self.backgroundIconColor = backgroundIconColor self.backgroundIconColor = backgroundIconColor
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.separatorColor = separatorColor self.separatorColor = separatorColor
@ -66,6 +69,9 @@ public final class EmojiSelectionComponent: Component {
if lhs.emojiContent != rhs.emojiContent { if lhs.emojiContent != rhs.emojiContent {
return false return false
} }
if lhs.stickerContent != rhs.stickerContent {
return false
}
if lhs.backgroundIconColor != rhs.backgroundIconColor { if lhs.backgroundIconColor != rhs.backgroundIconColor {
return false return false
} }
@ -96,6 +102,8 @@ public final class EmojiSelectionComponent: Component {
private var component: EmojiSelectionComponent? private var component: EmojiSelectionComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
private var isSearchActive: Bool = false
override init(frame: CGRect) { override init(frame: CGRect) {
self.keyboardView = ComponentView<Empty>() self.keyboardView = ComponentView<Empty>()
self.keyboardClippingView = UIView() self.keyboardClippingView = UIView()
@ -144,6 +152,12 @@ public final class EmojiSelectionComponent: Component {
deinit { 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 { func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.backgroundColor = component.backgroundColor self.backgroundColor = component.backgroundColor
let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85)
@ -154,6 +168,11 @@ public final class EmojiSelectionComponent: Component {
self.component = component self.component = component
self.state = state 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 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))) 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) 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 let backspaceButtonView = self.backspaceButton.view {
if backspaceButtonView.superview == nil { if backspaceButtonView.superview == nil {
@ -220,6 +239,7 @@ public final class EmojiSelectionComponent: Component {
} }
} }
self.keyboardView.parentState = state
let keyboardSize = self.keyboardView.update( let keyboardSize = self.keyboardView.update(
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
component: AnyComponent(EntityKeyboardComponent( component: AnyComponent(EntityKeyboardComponent(
@ -228,8 +248,8 @@ public final class EmojiSelectionComponent: Component {
isContentInFocus: true, isContentInFocus: true,
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: component.sideInset, bottom: component.bottomInset + 16.0, right: component.sideInset), 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), topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
emojiContent: component.emojiContent.withCustomTintColor(component.theme.list.itemPrimaryTextColor), emojiContent: component.emojiContent?.withCustomTintColor(component.theme.list.itemPrimaryTextColor),
stickerContent: nil, stickerContent: component.stickerContent?.withCustomTintColor(component.theme.list.itemPrimaryTextColor),
maskContent: nil, maskContent: nil,
gifContent: nil, gifContent: nil,
hasRecentGifs: false, hasRecentGifs: false,
@ -241,7 +261,15 @@ public final class EmojiSelectionComponent: Component {
topPanelExtensionUpdated: { _, _ in }, topPanelExtensionUpdated: { _, _ in },
topPanelScrollingOffset: { _, _ in }, topPanelScrollingOffset: { _, _ in },
hideInputUpdated: { _, _, _ 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: {}, switchToTextInput: {},
switchToGifSubject: { _ in }, switchToGifSubject: { _ in },
reorderItems: { _, _ in }, reorderItems: { _, _ in },
@ -257,7 +285,7 @@ public final class EmojiSelectionComponent: Component {
customTintColor: component.backgroundIconColor customTintColor: component.backgroundIconColor
)), )),
environment: {}, environment: {},
containerSize: availableSize containerSize: CGSize(width: availableSize.width, height: resolvedHeight)
) )
if let keyboardComponentView = self.keyboardView.view { if let keyboardComponentView = self.keyboardView.view {
if keyboardComponentView.superview == nil { if keyboardComponentView.superview == nil {
@ -270,7 +298,7 @@ public final class EmojiSelectionComponent: Component {
self.keyboardClippingView.clipsToBounds = false 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: 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))) 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) transition.setAlpha(view: self.panelSeparatorView, alpha: 1.0)
} }
return availableSize return CGSize(width: availableSize.width, height: resolvedHeight)
} }
} }

View File

@ -884,6 +884,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
bottomInset: environment.safeInsets.bottom, bottomInset: environment.safeInsets.bottom,
deviceMetrics: environment.deviceMetrics, deviceMetrics: environment.deviceMetrics,
emojiContent: emojiContent.withSelectedItems(Set(enabledReactions.map(\.file.fileId))), emojiContent: emojiContent.withSelectedItems(Set(enabledReactions.map(\.file.fileId))),
stickerContent: nil,
backgroundIconColor: nil, backgroundIconColor: nil,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor, backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
separatorColor: environment.theme.list.itemBlocksSeparatorColor, separatorColor: environment.theme.list.itemBlocksSeparatorColor,

View File

@ -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 { final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer() private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView private let scrollView: ScrollView
@ -74,12 +91,18 @@ final class BusinessIntroSetupScreenComponent: Component {
private let textInputTag = NSObject() private let textInputTag = NSObject()
private var resetText: String? private var resetText: String?
private var previousHadInputHeight: Bool = false
private var recenterOnTag: NSObject? private var recenterOnTag: NSObject?
private var stickerFile: TelegramMediaFile? private var stickerFile: TelegramMediaFile?
private var stickerContent: EmojiPagerContentComponent? private var stickerContent: EmojiPagerContentComponent?
private var stickerContentDisposable: Disposable? private var stickerContentDisposable: Disposable?
private let stickerSearchDisposable = MetaDisposable()
private var stickerSearchState = EmojiSearchState(result: nil, isSearching: false)
private var displayStickerInput: Bool = false private var displayStickerInput: Bool = false
private var stickerSelectionControlDimView: UIView?
private var stickerSelectionControl: ComponentView<Empty>? private var stickerSelectionControl: ComponentView<Empty>?
override init(frame: CGRect) { 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 { func update(component: BusinessIntroSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {
@ -193,9 +223,10 @@ final class BusinessIntroSetupScreenComponent: Component {
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: nil, chatPeerId: nil,
hasSearch: false, hasSearch: true,
hasTrending: false, hasTrending: false,
forceHasPremium: true forceHasPremium: true,
searchIsPlaceholderOnly: false
) )
self.stickerContentDisposable = (stickerContent self.stickerContentDisposable = (stickerContent
|> deliverOnMainQueue).start(next: { [weak self] stickerContent in |> deliverOnMainQueue).start(next: { [weak self] stickerContent in
@ -216,16 +247,16 @@ final class BusinessIntroSetupScreenComponent: Component {
self.stickerFile = itemFile self.stickerFile = itemFile
self.displayStickerInput = false self.displayStickerInput = false
self.stickerSearchDisposable.set(nil)
self.stickerSearchState = EmojiSearchState(result: nil, isSearching: false)
if !self.isUpdating { if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.25)) self.state?.updated(transition: .spring(duration: 0.4))
} }
}, },
deleteBackwards: { deleteBackwards: nil,
}, openStickerSettings: nil,
openStickerSettings: { openFeatured: nil,
},
openFeatured: {
},
openSearch: { openSearch: {
}, },
addGroupAction: { _, _, _ in addGroupAction: { _, _, _ in
@ -243,9 +274,202 @@ final class BusinessIntroSetupScreenComponent: Component {
navigationController: { navigationController: {
return nil 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: { updateScrollingToItemGroup: {
}, },
@ -349,6 +573,7 @@ final class BusinessIntroSetupScreenComponent: Component {
autocapitalizationType: .none, autocapitalizationType: .none,
autocorrectionType: .no, autocorrectionType: .no,
characterLimit: 32, characterLimit: 32,
displayCharacterLimit: true,
allowEmptyLines: false, allowEmptyLines: false,
updated: { _ in updated: { _ in
}, },
@ -369,6 +594,7 @@ final class BusinessIntroSetupScreenComponent: Component {
autocapitalizationType: .none, autocapitalizationType: .none,
autocorrectionType: .no, autocorrectionType: .no,
characterLimit: 70, characterLimit: 70,
displayCharacterLimit: true,
allowEmptyLines: false, allowEmptyLines: false,
updated: { _ in updated: { _ in
}, },
@ -416,6 +642,8 @@ final class BusinessIntroSetupScreenComponent: Component {
} }
self.displayStickerInput = true self.displayStickerInput = true
self.endEditing(true)
if !self.isUpdating { if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.5)) 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)) 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 let displayDelete = !self.titleInputState.text.string.isEmpty || !self.textInputState.text.string.isEmpty || self.stickerFile != nil
var deleteSectionHeight: CGFloat = 0.0 var deleteSectionHeight: CGFloat = 0.0
@ -557,6 +794,16 @@ final class BusinessIntroSetupScreenComponent: Component {
var inputHeight: CGFloat = environment.inputHeight var inputHeight: CGFloat = environment.inputHeight
if self.displayStickerInput, let stickerContent = self.stickerContent { 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> let stickerSelectionControl: ComponentView<Empty>
var animateIn = false var animateIn = false
if let current = self.stickerSelectionControl { if let current = self.stickerSelectionControl {
@ -570,22 +817,45 @@ final class BusinessIntroSetupScreenComponent: Component {
if let stickerFile = self.stickerFile { if let stickerFile = self.stickerFile {
selectedItems.insert(stickerFile.fileId) 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( let stickerSelectionControlSize = stickerSelectionControl.update(
transition: animateIn ? .immediate : transition, transition: stickerSelectionControlTransition,
component: AnyComponent(EmojiSelectionComponent( component: AnyComponent(EmojiSelectionComponent(
theme: environment.theme, theme: environment.theme,
strings: environment.strings, strings: environment.strings,
sideInset: environment.safeInsets.left, sideInset: environment.safeInsets.left,
bottomInset: environment.safeInsets.bottom, bottomInset: environment.safeInsets.bottom,
deviceMetrics: environment.deviceMetrics, deviceMetrics: environment.deviceMetrics,
emojiContent: stickerContent.withSelectedItems(selectedItems), emojiContent: nil,
stickerContent: stickerContent.withSelectedItems(selectedItems),
backgroundIconColor: nil, backgroundIconColor: nil,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor, backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
separatorColor: environment.theme.list.itemBlocksSeparatorColor, separatorColor: environment.theme.list.itemBlocksSeparatorColor,
backspace: nil backspace: nil
)), )),
environment: {}, 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) let stickerSelectionControlFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - stickerSelectionControlSize.height), size: stickerSelectionControlSize)
if let stickerSelectionControlView = stickerSelectionControl.view { if let stickerSelectionControlView = stickerSelectionControl.view {
@ -600,12 +870,18 @@ final class BusinessIntroSetupScreenComponent: Component {
} }
} }
inputHeight = stickerSelectionControlSize.height inputHeight = stickerSelectionControlSize.height
} else if let stickerSelectionControl = self.stickerSelectionControl { } else {
self.stickerSelectionControl = nil if let stickerSelectionControl = self.stickerSelectionControl {
if let stickerSelectionControlView = stickerSelectionControl.view { self.stickerSelectionControl = nil
transition.setPosition(view: stickerSelectionControlView, position: CGPoint(x: stickerSelectionControlView.center.x, y: availableSize.height + stickerSelectionControlView.bounds.height * 0.5), completion: { [weak stickerSelectionControlView] _ in if let stickerSelectionControlView = stickerSelectionControl.view {
stickerSelectionControlView?.removeFromSuperview() 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()
} }
} }

View File

@ -257,9 +257,15 @@ final class ChatbotSetupScreenComponent: Component {
if !query.isEmpty { if !query.isEmpty {
if self.botResolutionState?.query != query { if self.botResolutionState?.query != query {
let previousState = self.botResolutionState?.state 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( self.botResolutionState = BotResolutionState(
query: query, query: query,
state: self.botResolutionState?.state ?? .searching state: updatedState
) )
self.botResolutionDisposable?.dispose() self.botResolutionDisposable?.dispose()
@ -267,7 +273,19 @@ final class ChatbotSetupScreenComponent: Component {
self.state?.updated(transition: .spring(duration: 0.35)) 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 |> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else { guard let self else {
return return
@ -661,10 +679,20 @@ final class ChatbotSetupScreenComponent: Component {
guard let self else { guard let self else {
return return
} }
self.endEditing(true)
if var botResolutionState = self.botResolutionState, case let .found(peer, isInstalled) = botResolutionState.state, !isInstalled { if var botResolutionState = self.botResolutionState, case let .found(peer, isInstalled) = botResolutionState.state, !isInstalled {
botResolutionState.state = .found(peer: peer, isInstalled: true) if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.isBusiness) {
self.botResolutionState = botResolutionState botResolutionState.state = .found(peer: peer, isInstalled: true)
self.state?.updated(transition: .spring(duration: 0.3)) 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 removeAction: { [weak self] in