diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD index c9ad95f646..405bbcd734 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/Markdown", "//submodules/ReactionSelectionNode", "//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem", + "//submodules/PremiumUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index ef8be4e20f..bc9ad9d698 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -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) } } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index a3642584ed..8da9ddabec 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 3abfa3e7f2..8729adcaa9 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -272,7 +272,7 @@ public final class EntityKeyboardComponent: Component { private let pagerView: ComponentHostView private var component: EntityKeyboardComponent? - private weak var state: EmptyComponentState? + public private(set) weak var state: EmptyComponentState? private var searchView: ComponentHostView? private var searchComponent: EntitySearchContentComponent? diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index dee2bf8e5c..2f91e792de 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -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() + private var measureTextLimitLabel: ComponentView? + private var textLimitLabel: ComponentView? + 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 + 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 + 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 } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift index 44f8bb7dbe..0fa2c00af1 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift @@ -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() 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, 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) } } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index b5b550cda8..a5e5ee87a0 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift index fabdb80459..be316dc4ed 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift @@ -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? 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, 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 = .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() + 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() + 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 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() } } diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index f48a0a066d..6846aefb26 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -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