diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 75d79ef028..e07d7be3a3 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1118,3 +1118,23 @@ public struct StoriesConfiguration { //#endif } } + +public struct StickersSearchConfiguration { + static var defaultValue: StickersSearchConfiguration { + return StickersSearchConfiguration(disableLocalSuggestions: false) + } + + public let disableLocalSuggestions: Bool + + fileprivate init(disableLocalSuggestions: Bool) { + self.disableLocalSuggestions = disableLocalSuggestions + } + + public static func with(appConfiguration: AppConfiguration) -> StickersSearchConfiguration { + if let data = appConfiguration.data, let suggestOnlyApi = data["stickers_emoji_suggest_only_api"] as? Bool { + return StickersSearchConfiguration(disableLocalSuggestions: suggestOnlyApi) + } else { + return .defaultValue + } + } +} diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 8ef0550ea6..39706976f6 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -184,6 +184,7 @@ open class ViewControllerComponentContainer: ViewController { environment: { environment }, + forceUpdate: self.controller?.forceNextUpdate ?? false, containerSize: layout.size ) transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) @@ -306,6 +307,13 @@ open class ViewControllerComponentContainer: ViewController { super.dismiss(animated: flag, completion: completion) } + fileprivate var forceNextUpdate = false + public func requestLayout(forceUpdate: Bool, transition: ContainedViewLayoutTransition) { + self.forceNextUpdate = forceUpdate + self.requestLayout(transition: transition) + self.forceNextUpdate = false + } + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 368cd33e46..df1ba46eb8 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -301,7 +301,8 @@ private final class StickerSelectionComponent: Component { inputHeight: 0.0, displayBottomPanel: true, isExpanded: true, - clipContentToTopPanel: false + clipContentToTopPanel: false, + useExternalSearchContainer: false )), environment: {}, forceUpdate: self.forceUpdate, diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 0d1f0c32e2..6207cf95f3 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -1272,7 +1272,8 @@ final class AvatarEditorScreenComponent: Component { inputHeight: 0.0, displayBottomPanel: false, isExpanded: true, - clipContentToTopPanel: false + clipContentToTopPanel: false, + useExternalSearchContainer: false )), environment: {}, containerSize: CGSize(width: keyboardContainerFrame.size.width, height: keyboardContainerFrame.size.height - 6.0 + (isSearchActive ? 40.0 : 0.0)) diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 1edbcc6490..bd0cb6fbe5 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -406,6 +406,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return true } + public var useExternalSearchContainer: Bool = false + private final class GifContext { private var componentValue: EntityKeyboardGifContent? { didSet { @@ -1608,10 +1610,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { hideBackground: false, stateContext: nil ) + self.inputDataDisposable = (combineLatest(queue: .mainQueue(), updatedInputData, - self.gifComponent.get(), + .single(self.currentInputData.gifs) |> then(self.gifComponent.get() |> map(Optional.init)), self.emojiSearchState.get(), self.stickerSearchState.get() ) @@ -1980,11 +1983,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { mappedMode = .gif } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } let searchContainerNode = PaneSearchContainerNode( context: context, - theme: presentationData.theme, - strings: presentationData.strings, + theme: interfaceState.theme, + strings: interfaceState.strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction, mode: mappedMode, @@ -2008,7 +2010,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { inputHeight: inputHeight, displayBottomPanel: true, isExpanded: isExpanded && !self.isEmojiSearchActive, - clipContentToTopPanel: self.clipContentToTopPanel + clipContentToTopPanel: self.clipContentToTopPanel, + useExternalSearchContainer: self.useExternalSearchContainer )), environment: {}, containerSize: CGSize(width: width, height: expandedHeight) diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 94f96cd074..dd03395a50 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -191,7 +191,8 @@ public final class EmojiStatusSelectionComponent: Component { inputHeight: 0.0, displayBottomPanel: false, isExpanded: false, - clipContentToTopPanel: false + clipContentToTopPanel: false, + useExternalSearchContainer: false )), environment: {}, containerSize: availableSize diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 0cc2d25398..98dd4a8789 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -5201,7 +5201,7 @@ public final class EmojiPagerContentComponent: Component { scrollView.layer.removeAllAnimations() } - if self.isSearchActivated, let component = self.component, component.searchState == .empty(hasResults: false), !component.searchAlwaysActive, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil { + if self.isSearchActivated, let component = self.component, component.searchState == .empty(hasResults: true), !component.searchAlwaysActive, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil { scrollView.isScrollEnabled = false DispatchQueue.main.async { scrollView.isScrollEnabled = true @@ -6176,6 +6176,10 @@ public final class EmojiPagerContentComponent: Component { if let topVisibleGroupId = topVisibleGroupId { self.activeItemUpdated?.invoke((topVisibleGroupId, topVisibleSubgroupId, .immediate)) } + + if let fadingMaskLayer = self.fadingMaskLayer { + fadingMaskLayer.internalAlpha = max(0.0, min(1.0, self.scrollView.contentOffset.y / 30.0)) + } } private func updateShimmerIfNeeded() { @@ -6254,7 +6258,7 @@ public final class EmojiPagerContentComponent: Component { if self.layer.mask == nil { self.layer.mask = maskLayer } - maskLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: (topPanelHeight - 34.0) * 0.75), size: backgroundFrame.size) + maskLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((topPanelHeight - 34.0) * 0.75)), size: backgroundFrame.size) } else if component.warpContentsOnEdges { self.backgroundView.isHidden = true } else { @@ -6772,10 +6776,13 @@ public final class EmojiPagerContentComponent: Component { let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition) - if !useOpaqueTheme { - transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame) - transition.attachAnimation(view: visibleSearchHeader, id: "search_transition", completion: { [weak self] completed in - guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { + + transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame) + // Temporary workaround for status selection; use a separate search container (see GIF) + + if case let .curve(duration, _) = transition.animation, duration != 0.0 { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration, execute: { [weak self] in + guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else { return } @@ -6785,37 +6792,9 @@ public final class EmojiPagerContentComponent: Component { } }) } else { - transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in - if !useOpaqueTheme { - guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { - return - } - - if !strongSelf.isSearchActivated && visibleSearchHeader.superview != strongSelf.scrollView { - strongSelf.scrollView.addSubview(visibleSearchHeader) - strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) - } - } - }) - // Temporary workaround for status selection; use a separate search container (see GIF) - if useOpaqueTheme { - if case let .curve(duration, _) = transition.animation, duration != 0.0 { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration, execute: { [weak self] in - guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else { - return - } - - if !strongSelf.isSearchActivated && visibleSearchHeader.superview != strongSelf.scrollView { - strongSelf.scrollView.addSubview(visibleSearchHeader) - strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) - } - }) - } else { - if !self.isSearchActivated && visibleSearchHeader.superview != self.scrollView { - self.scrollView.addSubview(visibleSearchHeader) - self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) - } - } + if !self.isSearchActivated && visibleSearchHeader.superview != self.scrollView { + self.scrollView.addSubview(visibleSearchHeader) + self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) } } } else { @@ -8553,20 +8532,30 @@ func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor], tit private final class FadingMaskLayer: SimpleLayer { let gradientLayer = SimpleLayer() let fillLayer = SimpleLayer() + let gradientFillLayer = SimpleLayer() + + var internalAlpha: CGFloat = 1.0 { + didSet { + self.gradientFillLayer.opacity = Float(1.0 - self.internalAlpha) + } + } override func layoutSublayers() { let gradientHeight: CGFloat = 66.0 if self.gradientLayer.contents == nil { self.addSublayer(self.gradientLayer) self.addSublayer(self.fillLayer) + self.addSublayer(self.gradientFillLayer) let gradientImage = generateGradientImage(size: CGSize(width: 1.0, height: gradientHeight), colors: [UIColor.white.withAlphaComponent(0.0), UIColor.white.withAlphaComponent(0.0), UIColor.white, UIColor.white], locations: [0.0, 0.4, 0.9, 1.0], direction: .vertical) self.gradientLayer.contents = gradientImage?.cgImage self.gradientLayer.contentsGravity = .resize self.fillLayer.backgroundColor = UIColor.white.cgColor + self.gradientFillLayer.backgroundColor = UIColor.white.cgColor } self.gradientLayer.frame = CGRect(origin: .zero, size: CGSize(width: self.bounds.width, height: gradientHeight)) + self.gradientFillLayer.frame = self.gradientLayer.frame self.fillLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: gradientHeight), size: CGSize(width: self.bounds.width, height: self.bounds.height - gradientHeight)) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift index a67b84d819..9210c7c64c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift @@ -506,6 +506,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode displayBottomPanel: false, isExpanded: false, clipContentToTopPanel: false, + useExternalSearchContainer: false, hidePanels: true )), environment: {}, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 041a72732c..4df77e79f9 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -121,6 +121,7 @@ public final class EntityKeyboardComponent: Component { public let displayBottomPanel: Bool public let isExpanded: Bool public let clipContentToTopPanel: Bool + public let useExternalSearchContainer: Bool public let hidePanels: Bool public init( @@ -153,6 +154,7 @@ public final class EntityKeyboardComponent: Component { displayBottomPanel: Bool, isExpanded: Bool, clipContentToTopPanel: Bool, + useExternalSearchContainer: Bool, hidePanels: Bool = false ) { self.theme = theme @@ -184,6 +186,7 @@ public final class EntityKeyboardComponent: Component { self.displayBottomPanel = displayBottomPanel self.isExpanded = isExpanded self.clipContentToTopPanel = clipContentToTopPanel + self.useExternalSearchContainer = useExternalSearchContainer self.hidePanels = hidePanels } @@ -248,6 +251,9 @@ public final class EntityKeyboardComponent: Component { if lhs.clipContentToTopPanel != rhs.clipContentToTopPanel { return false } + if lhs.useExternalSearchContainer != rhs.useExternalSearchContainer { + return false + } return true } @@ -908,14 +914,20 @@ public final class EntityKeyboardComponent: Component { contentType = .stickers } - self.searchComponent = EntitySearchContentComponent( - makeContainerNode: { - return component.makeSearchContainerNode(contentType) - }, - dismissSearch: { [weak self] in - self?.closeSearch() - } - ) + if component.useExternalSearchContainer, let containerNode = component.makeSearchContainerNode(contentType) { + let controller = EntitySearchContainerController(containerNode: containerNode) + + self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller) + } else { + self.searchComponent = EntitySearchContentComponent( + makeContainerNode: { + return component.makeSearchContainerNode(contentType) + }, + dismissSearch: { [weak self] in + self?.closeSearch() + } + ) + } //self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring))) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift index d6ccaed16b..53271205d2 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift @@ -18,6 +18,57 @@ public protocol EntitySearchContainerNode: ASDisplayNode { func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) } +public final class EntitySearchContainerController: ViewController { + private var node: Node { + return self.displayNode as! Node + } + + private let containerNode: EntitySearchContainerNode + + public init(containerNode: EntitySearchContainerNode) { + self.containerNode = containerNode + + super.init(navigationBarPresentationData: nil) + + self.navigationPresentation = .modal + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = Node(containerNode: self.containerNode, controller: self) + self.displayNodeDidLoad() + } + + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.node.containerLayoutUpdated(layout, transition: transition) + } + + private class Node: ViewControllerTracingNode, UIScrollViewDelegate { + private weak var controller: EntitySearchContainerController? + + private let containerNode: EntitySearchContainerNode + + init(containerNode: EntitySearchContainerNode, controller: EntitySearchContainerController) { + self.containerNode = containerNode + self.controller = controller + + super.init() + + self.addSubnode(containerNode) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.containerNode.updateLayout(size: layout.size, leftInset: 0.0, rightInset: 0.0, bottomInset: layout.intrinsicInsets.bottom, inputHeight: layout.inputHeight ?? 0.0, deviceMetrics: layout.deviceMetrics, transition: transition) + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: .zero, size: layout.size)) + } + } +} + final class EntitySearchContentEnvironment: Equatable { let context: AccountContext let theme: PresentationTheme diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 6a2be5e358..b8703e34e7 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -364,7 +364,8 @@ private final class TopicIconSelectionComponent: Component { inputHeight: 0.0, displayBottomPanel: false, isExpanded: true, - clipContentToTopPanel: false + clipContentToTopPanel: false, + useExternalSearchContainer: false )), environment: {}, containerSize: availableSize diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 6c8dc47e21..08f8f73b7c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/TelegramUI/Components/MessageInputPanelComponent", + "//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/ChatInputNode", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/TooltipUI", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 20765d669f..52f6253ca5 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -16,6 +16,7 @@ import MediaEditor import Photos import LottieAnimationComponent import MessageInputPanelComponent +import TextFieldComponent import EntityKeyboard import TooltipUI import BlurredBackgroundComponent @@ -268,7 +269,7 @@ final class MediaEditorScreenComponent: Component { self.backgroundColor = .clear self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) - self.fadeView.addTarget(self, action: #selector(self.fadePressed), for: .touchUpInside) + self.fadeView.addTarget(self, action: #selector(self.deactivateInput), for: .touchUpInside) self.fadeView.alpha = 0.0 self.addSubview(self.fadeView) @@ -305,7 +306,7 @@ final class MediaEditorScreenComponent: Component { context: context, chatPeerId: nil, areCustomEmojiEnabled: true, - hasSearch: false, + hasSearch: true, hideBackground: true, sendGif: nil ) |> map { inputData -> ChatEntityKeyboardInputNode.InputData in @@ -336,8 +337,7 @@ final class MediaEditorScreenComponent: Component { updateChoosingSticker: { _ in }, switchToTextInput: { [weak self] in if let self { - self.currentInputMode = .text - self.state?.updated(transition: .immediate) + self.activateInput() } }, dismissTextInput: { @@ -366,7 +366,7 @@ final class MediaEditorScreenComponent: Component { getNavigationController: { return nil }, requestLayout: { [weak self] transition in if let self { - self.environment?.controller()?.requestLayout(transition: transition) + (self.environment?.controller() as? MediaEditorScreen)?.node.requestLayout(forceUpdate: true, transition: Transition(transition)) } } ) @@ -374,9 +374,24 @@ final class MediaEditorScreenComponent: Component { } } - @objc private func fadePressed() { + private func activateInput() { self.currentInputMode = .text - self.endEditing(true) + if !hasFirstResponder(self) { + if let view = self.inputPanel.view as? MessageInputPanelComponent.View { + view.activateInput() + } + } else { + self.state?.updated(transition: .immediate) + } + } + + @objc private func deactivateInput() { + self.currentInputMode = .text + if hasFirstResponder(self) { + self.endEditing(true) + } else { + self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged))) + } } private var animatingButtons = false @@ -739,7 +754,7 @@ final class MediaEditorScreenComponent: Component { let buttonsAvailableWidth: CGFloat let buttonsLeftOffset: CGFloat if isTablet { - buttonsAvailableWidth = previewSize.width + 260.0 + buttonsAvailableWidth = previewSize.width + 180.0 buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0) } else { buttonsAvailableWidth = floor(availableSize.width - cancelButtonSize.width * 0.66 - (doneButtonSize.width - cancelButtonSize.width * 0.33) - buttonSideInset * 2.0) @@ -950,6 +965,92 @@ final class MediaEditorScreenComponent: Component { inputPanelAvailableHeight = 200.0 } + var inputHeight = environment.inputHeight + var keyboardHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false) + let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden + + if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + let inputMediaNode: ChatEntityKeyboardInputNode + if let current = self.inputMediaNode { + inputMediaNode = current + } else { + inputMediaNode = ChatEntityKeyboardInputNode( + context: component.context, + currentInputData: inputData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: true, + opaqueTopPanelBackground: false, + interaction: self.inputMediaInteraction, + chatPeerId: nil, + stateContext: self.inputMediaNodeStateContext + ) + inputMediaNode.externalTopPanelContainerImpl = nil + if let inputPanelView = self.inputPanel.view, inputMediaNode.view.superview == nil { + self.insertSubview(inputMediaNode.view, belowSubview: inputPanelView) + } + self.inputMediaNode = inputMediaNode + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .builtin(WallpaperSettings()), + theme: presentationData.theme, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 }, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + accountPeerId: component.context.account.peerId, + mode: .standard(previewing: false), + chatLocation: .peer(id: component.context.account.peerId), + subject: nil, + peerNearbyData: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + importState: nil, + threadData: nil, + isGeneralThreadClosed: nil + ) + + let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: component.bottomSafeInset, standardInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: environment.inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: environment.metrics, deviceMetrics: environment.deviceMetrics, isVisible: true, isExpanded: false) + let inputNodeHeight = heightAndOverflow.0 + let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) + transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + + inputHeight = heightAndOverflow.0 + keyboardHeight = max(keyboardHeight, heightAndOverflow.0) + } else if let inputMediaNode = self.inputMediaNode { + self.inputMediaNode = nil + + var dismissingInputHeight = environment.inputHeight + if self.currentInputMode == .emoji || (dismissingInputHeight.isZero && keyboardWasHidden) { + dismissingInputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false)) + } + + if let animationHint = transition.userData(TextFieldComponent.AnimationHint.self), case .textFocusChanged = animationHint.kind { + dismissingInputHeight = 0.0 + } + + var targetFrame = inputMediaNode.frame + if dismissingInputHeight > 0.0 { + targetFrame.origin.y = availableSize.height - dismissingInputHeight + } else { + targetFrame.origin.y = availableSize.height + } + transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in + if let inputMediaNode { + Queue.mainQueue().after(0.2) { + inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in + inputMediaNode?.view.removeFromSuperview() + }) + } + } + }) + } + let nextInputMode: MessageInputPanelComponent.InputMode switch self.currentInputMode { case .text: @@ -960,7 +1061,6 @@ final class MediaEditorScreenComponent: Component { nextInputMode = .emoji } - let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden self.inputPanel.parentState = state let inputPanelSize = self.inputPanel.update( transition: transition, @@ -980,13 +1080,19 @@ final class MediaEditorScreenComponent: Component { } controller.present(c, in: .window(.root)) }, + presentInGlobalOverlay: {[weak self] c in + guard let self, let _ = self.component, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen else { + return + } + controller.presentInGlobalOverlay(c) + }, sendMessageAction: { [weak self] in guard let self else { return } - self.currentInputMode = .text - self.endEditing(true) + self.deactivateInput() }, + sendStickerAction: { _ in }, setMediaRecordingActive: nil, lockMediaRecording: nil, stopAndPreviewMediaRecording: nil, @@ -1002,7 +1108,11 @@ final class MediaEditorScreenComponent: Component { default: self.currentInputMode = .emoji } - self.state?.updated(transition: .immediate) + if self.currentInputMode == .text { + self.activateInput() + } else { + self.state?.updated(transition: .immediate) + } } }, timeoutAction: isEditingStory ? nil : { [weak self] view in @@ -1034,12 +1144,19 @@ final class MediaEditorScreenComponent: Component { displayGradient: false, bottomInset: 0.0, hideKeyboard: self.currentInputMode == .emoji, + forceIsEditing: self.currentInputMode == .emoji, disabledPlaceholder: nil )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) ) + if self.inputPanelExternalState.isEditing { + if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) { + inputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false)) + } + } + let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) if self.inputPanelExternalState.isEditing { fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0) @@ -1061,19 +1178,12 @@ final class MediaEditorScreenComponent: Component { mediaEditor?.play() } } - - var inputHeight = environment.inputHeight - if self.inputPanelExternalState.isEditing { - if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) { - inputHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false) - } - } - + let inputPanelBackgroundSize = self.inputPanelBackground.update( transition: transition, component: AnyComponent(BlurredGradientComponent(position: .bottom, tag: nil)), environment: {}, - containerSize: CGSize(width: availableSize.width, height: environment.deviceMetrics.standardInputHeight(inLandscape: false) + 100.0) + containerSize: CGSize(width: availableSize.width, height: keyboardHeight + 100.0) ) if let inputPanelBackgroundView = self.inputPanelBackground.view { if inputPanelBackgroundView.superview == nil { @@ -1414,76 +1524,6 @@ final class MediaEditorScreenComponent: Component { transition.setAlpha(view: textSizeView, alpha: sizeSliderVisible && !component.isInteractingWithEntities ? 1.0 : 0.0) } - if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { - let inputMediaNode: ChatEntityKeyboardInputNode - if let current = self.inputMediaNode { - inputMediaNode = current - } else { - inputMediaNode = ChatEntityKeyboardInputNode( - context: component.context, - currentInputData: inputData, - updatedInputData: self.inputMediaNodeDataPromise.get(), - defaultToEmojiTab: true, - opaqueTopPanelBackground: false, - interaction: self.inputMediaInteraction, - chatPeerId: nil, - stateContext: self.inputMediaNodeStateContext - ) - inputMediaNode.externalTopPanelContainerImpl = nil - if let inputPanelView = self.inputPanel.view, inputMediaNode.view.superview == nil { - self.insertSubview(inputMediaNode.view, belowSubview: inputPanelView) - } - self.inputMediaNode = inputMediaNode - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - let presentationInterfaceState = ChatPresentationInterfaceState( - chatWallpaper: .builtin(WallpaperSettings()), - theme: presentationData.theme, - strings: presentationData.strings, - dateTimeFormat: presentationData.dateTimeFormat, - nameDisplayOrder: presentationData.nameDisplayOrder, - limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 }, - fontSize: presentationData.chatFontSize, - bubbleCorners: presentationData.chatBubbleCorners, - accountPeerId: component.context.account.peerId, - mode: .standard(previewing: false), - chatLocation: .peer(id: component.context.account.peerId), - subject: nil, - peerNearbyData: nil, - greetingData: nil, - pendingUnpinnedAllMessages: false, - activeGroupCallInfo: nil, - hasActiveGroupCall: false, - importState: nil, - threadData: nil, - isGeneralThreadClosed: nil - ) - - let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: component.bottomSafeInset, standardInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: environment.inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: environment.metrics, deviceMetrics: environment.deviceMetrics, isVisible: true, isExpanded: false) - let inputNodeHeight = heightAndOverflow.0 - let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) - transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) - } else if let inputMediaNode = self.inputMediaNode { - self.inputMediaNode = nil - - var targetFrame = inputMediaNode.frame - if inputHeight > 0.0 { - targetFrame.origin.y = availableSize.height - inputHeight - } else { - targetFrame.origin.y = availableSize.height - } - transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in - if let inputMediaNode { - Queue.mainQueue().after(0.3) { - inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in - inputMediaNode?.view.removeFromSuperview() - }) - } - } - }) - } - component.externalState.derivedInputHeight = inputHeight return availableSize @@ -1690,7 +1730,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate isStatusSelection: false, isReactionSelection: false, isEmojiSelection: true, - hasTrending: false, + hasTrending: true, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: true, @@ -2644,6 +2684,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var drawingScreen: DrawingScreen? private var stickerScreen: StickerPickerScreen? + func requestLayout(forceUpdate: Bool, transition: Transition) { + guard let layout = self.validLayout else { + return + } + self.containerLayoutUpdated(layout: layout, forceUpdate: forceUpdate, hasAppeared: self.hasAppeared, transition: transition) + } + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) { guard let controller = self.controller, !self.isDismissed else { return @@ -4293,3 +4340,15 @@ public final class BlurredGradientComponent: Component { func draftPath() -> String { return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts" } + +func hasFirstResponder(_ view: UIView) -> Bool { + if view.isFirstResponder { + return true + } + for subview in view.subviews { + if hasFirstResponder(subview) { + return true + } + } + return false +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 8328adea70..cc0390fdb7 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -253,10 +253,10 @@ final class StoryPreviewComponent: Component { alwaysDarkWhenHasText: false, nextInputMode: { _ in return .stickers }, areVoiceMessagesAvailable: false, - presentController: { _ in - }, - sendMessageAction: { - }, + presentController: { _ in }, + presentInGlobalOverlay: { _ in }, + sendMessageAction: { }, + sendStickerAction: { _ in }, setMediaRecordingActive: { _, _, _ in }, lockMediaRecording: nil, stopAndPreviewMediaRecording: nil, @@ -277,6 +277,7 @@ final class StoryPreviewComponent: Component { displayGradient: false, bottomInset: 0.0, hideKeyboard: false, + forceIsEditing: false, disabledPlaceholder: nil )), environment: {}, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index 354df30334..1951f54982 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -30,6 +30,8 @@ swift_library( "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/MoreHeaderButton", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/StickerPeekUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift index 62e55b96ec..64300518a6 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift @@ -8,30 +8,18 @@ import AccountContext import TelegramPresentationData import PeerListItemComponent -extension ChatPresentationInputQueryResult { - var count: Int { - switch self { - case let .stickers(stickers): - return stickers.count - case let .hashtags(hashtags): - return hashtags.count - case let .mentions(peers): - return peers.count - case let .commands(commands): - return commands.count - default: - return 0 - } - } - -} - final class ContextResultPanelComponent: Component { - final class ExternalState { - fileprivate(set) var minimizedHeight: CGFloat = 0.0 - fileprivate(set) var effectiveHeight: CGFloat = 0.0 - - init() { + enum Results: Equatable { + case mentions([EnginePeer]) + case hashtags([String]) + + var count: Int { + switch self { + case let .hashtags(hashtags): + return hashtags.count + case let .mentions(peers): + return peers.count + } } } @@ -40,22 +28,19 @@ final class ContextResultPanelComponent: Component { case hashtag(String) } - let externalState: ExternalState let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings - let results: ChatPresentationInputQueryResult + let results: Results let action: (ResultAction) -> Void init( - externalState: ExternalState, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, - results: ChatPresentationInputQueryResult, + results: Results, action: @escaping (ResultAction) -> Void ) { - self.externalState = externalState self.context = context self.theme = theme self.strings = strings @@ -64,9 +49,6 @@ final class ContextResultPanelComponent: Component { } static func ==(lhs: ContextResultPanelComponent, rhs: ContextResultPanelComponent) -> Bool { - if lhs.externalState !== rhs.externalState { - return false - } if lhs.context !== rhs.context { return false } @@ -87,27 +69,27 @@ final class ContextResultPanelComponent: Component { var bottomInset: CGFloat var topInset: CGFloat var sideInset: CGFloat - var itemHeight: CGFloat + var itemSize: CGSize var itemCount: Int var contentSize: CGSize - init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int) { + init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemSize: CGSize, itemCount: Int) { self.containerSize = containerSize self.bottomInset = bottomInset self.topInset = topInset self.sideInset = sideInset - self.itemHeight = itemHeight + self.itemSize = itemSize self.itemCount = itemCount - self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemHeight + bottomInset) + self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemSize.height + bottomInset) } func visibleItems(for rect: CGRect) -> Range? { let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset) - var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight))) + var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemSize.height))) minVisibleRow = max(0, minVisibleRow) - let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight))) + let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemSize.height))) let minVisibleIndex = minVisibleRow let maxVisibleIndex = maxVisibleRow @@ -120,7 +102,7 @@ final class ContextResultPanelComponent: Component { } func itemFrame(for index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: 0.0, y: self.topInset + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width, height: self.itemHeight)) + return CGRect(origin: CGPoint(x: 0.0, y: self.topInset + CGFloat(index) * self.itemSize.height), size: CGSize(width: self.containerSize.width, height: self.itemSize.height)) } } @@ -159,7 +141,7 @@ final class ContextResultPanelComponent: Component { self.scrollView = ScrollView() self.scrollView.canCancelContentTouches = true self.scrollView.delaysContentTouches = false - self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsVerticalScrollIndicator = false self.scrollView.contentInsetAdjustmentBehavior = .never self.scrollView.alwaysBounceVertical = true self.scrollView.indicatorStyle = .white @@ -329,7 +311,7 @@ final class ContextResultPanelComponent: Component { bottomInset: 0.0, topInset: 0.0, sideInset: sideInset, - itemHeight: measureItemSize.height, + itemSize: measureItemSize, itemCount: component.results.count ) self.itemLayout = itemLayout @@ -357,11 +339,6 @@ final class ContextResultPanelComponent: Component { self.ignoreScrolling = false self.updateScrolling(transition: transition) - -// component.externalState.minimizedHeight = minimizedHeight - -// let effectiveHeight: CGFloat = minimizedHeight * dismissFraction + (1.0 - dismissFraction) * (60.0 + component.safeInsets.bottom + 1.0) -// component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, effectiveHeight)) return availableSize } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift index 584bd0d883..b143e127d1 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift @@ -4,6 +4,7 @@ import TelegramCore import TextFieldComponent import ChatContextQuery import AccountContext +import TelegramUIPreferences func textInputStateContextQueryRangeAndType(inputState: TextFieldComponent.InputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] { return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange) @@ -40,7 +41,7 @@ func inputContextQueries(_ inputState: TextFieldComponent.InputState) -> [ChatPr func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] { let inputQueries = inputContextQueries(inputState).filter({ query in switch query { - case .contextRequest, .command, .emoji: + case .contextRequest, .command: return false default: return true @@ -75,6 +76,57 @@ func contextQueryResultState(context: AccountContext, inputState: TextFieldCompo private func updatedContextQueryResultStateForQuery(context: AccountContext, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> { switch inputQuery { + case let .emoji(query): + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() + if let previousQuery = previousQuery { + switch previousQuery { + case .emoji: + break + default: + signal = .single({ _ in return .stickers([]) }) + } + } else { + signal = .single({ _ in return .stickers([]) }) + } + + let stickerConfiguration = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + |> map { preferencesView -> StickersSearchConfiguration in + let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + return StickersSearchConfiguration.with(appConfiguration: appConfiguration) + } + let stickerSettings = context.sharedContext.accountManager.transaction { transaction -> StickerSettings in + let stickerSettings: StickerSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.stickerSettings)?.get(StickerSettings.self) ?? .defaultSettings + return stickerSettings + } + + let stickers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = combineLatest(stickerConfiguration, stickerSettings) + |> castError(ChatContextQueryError.self) + |> mapToSignal { stickerConfiguration, stickerSettings -> Signal<[FoundStickerItem], ChatContextQueryError> in + let scope: SearchStickersScope + switch stickerSettings.emojiStickerSuggestionMode { + case .none: + scope = [] + case .all: + if stickerConfiguration.disableLocalSuggestions { + scope = [.remote] + } else { + scope = [.installed, .remote] + } + case .installed: + scope = [.installed] + } + return context.engine.stickers.searchStickers(query: [query.basicEmoji.0], scope: scope) + |> map { items -> [FoundStickerItem] in + return items.items + } + |> castError(ChatContextQueryError.self) + } + |> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + return { _ in + return .stickers(stickers) + } + } + return signal |> then(stickers) case let .hashtag(query): var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() if let previousQuery = previousQuery { diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 26881bf0aa..053d3f3518 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -52,7 +52,9 @@ public final class MessageInputPanelComponent: Component { public let nextInputMode: (Bool) -> InputMode? public let areVoiceMessagesAvailable: Bool public let presentController: (ViewController) -> Void + public let presentInGlobalOverlay: (ViewController) -> Void public let sendMessageAction: () -> Void + public let sendStickerAction: (TelegramMediaFile) -> Void public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)? public let lockMediaRecording: (() -> Void)? public let stopAndPreviewMediaRecording: (() -> Void)? @@ -73,6 +75,7 @@ public final class MessageInputPanelComponent: Component { public let displayGradient: Bool public let bottomInset: CGFloat public let hideKeyboard: Bool + public let forceIsEditing: Bool public let disabledPlaceholder: String? public init( @@ -86,7 +89,9 @@ public final class MessageInputPanelComponent: Component { nextInputMode: @escaping (Bool) -> InputMode?, areVoiceMessagesAvailable: Bool, presentController: @escaping (ViewController) -> Void, + presentInGlobalOverlay: @escaping (ViewController) -> Void, sendMessageAction: @escaping () -> Void, + sendStickerAction: @escaping (TelegramMediaFile) -> Void, setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?, lockMediaRecording: (() -> Void)?, stopAndPreviewMediaRecording: (() -> Void)?, @@ -107,6 +112,7 @@ public final class MessageInputPanelComponent: Component { displayGradient: Bool, bottomInset: CGFloat, hideKeyboard: Bool, + forceIsEditing: Bool, disabledPlaceholder: String? ) { self.externalState = externalState @@ -119,7 +125,9 @@ public final class MessageInputPanelComponent: Component { self.alwaysDarkWhenHasText = alwaysDarkWhenHasText self.areVoiceMessagesAvailable = areVoiceMessagesAvailable self.presentController = presentController + self.presentInGlobalOverlay = presentInGlobalOverlay self.sendMessageAction = sendMessageAction + self.sendStickerAction = sendStickerAction self.setMediaRecordingActive = setMediaRecordingActive self.lockMediaRecording = lockMediaRecording self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording @@ -140,6 +148,7 @@ public final class MessageInputPanelComponent: Component { self.displayGradient = displayGradient self.bottomInset = bottomInset self.hideKeyboard = hideKeyboard + self.forceIsEditing = forceIsEditing self.disabledPlaceholder = disabledPlaceholder } @@ -204,6 +213,9 @@ public final class MessageInputPanelComponent: Component { if lhs.hideKeyboard != rhs.hideKeyboard { return false } + if lhs.forceIsEditing != rhs.forceIsEditing { + return false + } if lhs.disabledPlaceholder != rhs.disabledPlaceholder { return false } @@ -248,7 +260,8 @@ public final class MessageInputPanelComponent: Component { private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:] private var contextQueryResultPanel: ComponentView? - private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState? + + private var stickersResultPanel: ComponentView? private var viewForOverlayContent: ViewForOverlayContent? private var currentEmojiSuggestionView: ComponentHostView? @@ -389,6 +402,10 @@ public final class MessageInputPanelComponent: Component { self.state?.updated() } + if result == nil, let stickersResultPanel = self.stickersResultPanel?.view, let panelResult = stickersResultPanel.hitTest(self.convert(point, to: stickersResultPanel), with: event), panelResult !== stickersResultPanel { + return panelResult + } + if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel { return panelResult } @@ -471,6 +488,8 @@ public final class MessageInputPanelComponent: Component { environment: {}, containerSize: availableTextFieldSize ) + let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing + let placeholderSize = self.placeholder.update( transition: .immediate, @@ -493,7 +512,7 @@ public final class MessageInputPanelComponent: Component { environment: {}, containerSize: availableTextFieldSize ) - if !self.textFieldExternalState.isEditing && component.setMediaRecordingActive == nil { + if !isEditing && component.setMediaRecordingActive == nil { insets.right = insets.left } @@ -518,7 +537,7 @@ public final class MessageInputPanelComponent: Component { transition.setAlpha(view: self.bottomGradientView, alpha: component.displayGradient ? 1.0 : 0.0) let placeholderOriginX: CGFloat - if self.textFieldExternalState.isEditing || component.style == .story { + if isEditing || component.style == .story { placeholderOriginX = 16.0 } else { placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0) @@ -729,14 +748,14 @@ public final class MessageInputPanelComponent: Component { let inputActionButtonMode: MessageInputActionButtonComponent.Mode if case .editor = component.style { - inputActionButtonMode = self.textFieldExternalState.isEditing ? .apply : .none + inputActionButtonMode = isEditing ? .apply : .none } else { if hasMediaEditing { inputActionButtonMode = .send } else { if self.textFieldExternalState.hasText { inputActionButtonMode = .send - } else if !self.textFieldExternalState.isEditing && component.forwardAction != nil { + } else if !isEditing && component.forwardAction != nil { inputActionButtonMode = .forward } else { if component.areVoiceMessagesAvailable { @@ -831,7 +850,7 @@ public final class MessageInputPanelComponent: Component { self.addSubview(inputActionButtonView) } let inputActionButtonOriginX: CGFloat - if component.setMediaRecordingActive != nil || self.textFieldExternalState.isEditing { + if component.setMediaRecordingActive != nil || isEditing { inputActionButtonOriginX = size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5) } else { inputActionButtonOriginX = size.width @@ -845,7 +864,7 @@ public final class MessageInputPanelComponent: Component { var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0 var inputModeVisible = false - if component.style == .story || self.textFieldExternalState.isEditing { + if component.style == .story || isEditing { inputModeVisible = true } @@ -996,15 +1015,15 @@ public final class MessageInputPanelComponent: Component { transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center) transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size)) - transition.setAlpha(view: timeoutButtonView, alpha: self.textFieldExternalState.isEditing ? 0.0 : 1.0) - transition.setScale(view: timeoutButtonView, scale: self.textFieldExternalState.isEditing ? 0.1 : 1.0) + transition.setAlpha(view: timeoutButtonView, alpha: isEditing ? 0.0 : 1.0) + transition.setScale(view: timeoutButtonView, scale: isEditing ? 0.1 : 1.0) } } var fieldBackgroundIsDark = false if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText { fieldBackgroundIsDark = true - } else if self.textFieldExternalState.isEditing || component.style == .editor { + } else if isEditing || component.style == .editor { fieldBackgroundIsDark = true } self.fieldBackgroundView.updateColor(color: fieldBackgroundIsDark ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition) @@ -1013,7 +1032,7 @@ public final class MessageInputPanelComponent: Component { vibrancyPlaceholderView.isHidden = placeholder.isHidden } - component.externalState.isEditing = self.textFieldExternalState.isEditing + component.externalState.isEditing = isEditing component.externalState.hasText = self.textFieldExternalState.hasText component.externalState.insertText = { [weak self] text in if let self, let view = self.textField.view as? TextFieldComponent.View { @@ -1177,63 +1196,128 @@ public final class MessageInputPanelComponent: Component { let panelLeftInset: CGFloat = max(insets.left, 7.0) let panelRightInset: CGFloat = max(insets.right, 41.0) - if let result = self.contextQueryResults[.mention], result.count > 0 && self.textFieldExternalState.isEditing { + var contextResults: ContextResultPanelComponent.Results? + if let result = self.contextQueryResults[.mention], case let .mentions(mentions) = result, !mentions.isEmpty { + contextResults = .mentions(mentions) + } + + if let result = self.contextQueryResults[.emoji], case let .stickers(stickers) = result, !stickers.isEmpty { let availablePanelHeight: CGFloat = 413.0 var animateIn = false let panel: ComponentView - let externalState: ContextResultPanelComponent.ExternalState var transition = transition - if let current = self.contextQueryResultPanel, let currentState = self.contextQueryResultPanelExternalState { + if let current = self.stickersResultPanel { + panel = current + } else { + panel = ComponentView() + self.stickersResultPanel = panel + animateIn = true + transition = .immediate + } + let panelSize = panel.update( + transition: transition, + component: AnyComponent(StickersResultPanelComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + files: stickers.map { $0.file }, + action: { [weak self] sticker in + if let self, let textView = self.textField.view as? TextFieldComponent.View { + textView.updateText(NSAttributedString(), selectionRange: 0 ..< 0) + self.component?.sendStickerAction(sticker) + } + }, + present: { [weak self] c in + if let self, let component = self.component { + component.presentController(c) + } + }, + presentInGlobalOverlay: { [weak self] c in + if let self, let component = self.component { + component.presentInGlobalOverlay(c) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availablePanelHeight) + ) + + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: -panelSize.height + 60.0), size: panelSize) + if let panelView = panel.view as? StickersResultPanelComponent.View { + if panelView.superview == nil { + self.insertSubview(panelView, at: 0) + } + transition.setFrame(view: panelView, frame: panelFrame) + + if animateIn { + panelView.animateIn(transition: .spring(duration: 0.4)) + } + } + } else if let stickersResultPanel = self.stickersResultPanel?.view as? StickersResultPanelComponent.View { + self.stickersResultPanel = nil + stickersResultPanel.animateOut(transition: .spring(duration: 0.4), completion: { [weak stickersResultPanel] in + stickersResultPanel?.removeFromSuperview() + }) + } + + if let contextResults, isEditing { + let availablePanelHeight: CGFloat = 413.0 + + var animateIn = false + let panel: ComponentView + var transition = transition + if let current = self.contextQueryResultPanel { panel = current - externalState = currentState } else { panel = ComponentView() - externalState = ContextResultPanelComponent.ExternalState() self.contextQueryResultPanel = panel - self.contextQueryResultPanelExternalState = externalState animateIn = true transition = .immediate } let panelSize = panel.update( transition: transition, component: AnyComponent(ContextResultPanelComponent( - externalState: externalState, context: component.context, theme: component.theme, strings: component.strings, - results: result, + results: contextResults, action: { [weak self] action in - if let self, case let .mention(peer) = action, let textView = self.textField.view as? TextFieldComponent.View { + if let self, let textView = self.textField.view as? TextFieldComponent.View { let inputState = textView.getInputState() - var mentionQueryRange: NSRange? - inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) { - if type == [.mention] { - mentionQueryRange = range - break inner + switch action { + case let .mention(peer): + var mentionQueryRange: NSRange? + inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) { + if type == [.mention] { + mentionQueryRange = range + break inner + } } - } - - if let range = mentionQueryRange { - let inputText = NSMutableAttributedString(attributedString: inputState.inputText) - if let addressName = peer.addressName, !addressName.isEmpty { - let replacementText = addressName + " " - inputText.replaceCharacters(in: range, with: replacementText) - - let selectionPosition = range.lowerBound + (replacementText as NSString).length - textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) - } else if !peer.compactDisplayTitle.isEmpty { - let replacementText = NSMutableAttributedString() - replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)])) - replacementText.append(NSAttributedString(string: " ")) - - let updatedRange = NSRange(location: range.location - 1, length: range.length + 1) - inputText.replaceCharacters(in: updatedRange, with: replacementText) - - let selectionPosition = updatedRange.lowerBound + replacementText.length - textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + + if let range = mentionQueryRange { + let inputText = NSMutableAttributedString(attributedString: inputState.inputText) + if let addressName = peer.addressName, !addressName.isEmpty { + let replacementText = addressName + " " + inputText.replaceCharacters(in: range, with: replacementText) + + let selectionPosition = range.lowerBound + (replacementText as NSString).length + textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + } else if !peer.compactDisplayTitle.isEmpty { + let replacementText = NSMutableAttributedString() + replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)])) + replacementText.append(NSAttributedString(string: " ")) + + let updatedRange = NSRange(location: range.location - 1, length: range.length + 1) + inputText.replaceCharacters(in: updatedRange, with: replacementText) + + let selectionPosition = updatedRange.lowerBound + replacementText.length + textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + } } + case let .hashtag(hashtag): + let _ = hashtag } } } @@ -1309,7 +1393,6 @@ public final class MessageInputPanelComponent: Component { //self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView) } - let globalPosition: CGPoint if let textView = self.textField.view { diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift new file mode 100644 index 0000000000..a4cb67b6d5 --- /dev/null +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift @@ -0,0 +1,528 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ComponentDisplayAdapters +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramPresentationData +import PeerListItemComponent +import EmojiTextAttachmentView +import TextFormat +import ContextUI +import StickerPeekUI +import UndoUI + +final class StickersResultPanelComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let files: [TelegramMediaFile] + let action: (TelegramMediaFile) -> Void + let present: (ViewController) -> Void + let presentInGlobalOverlay: (ViewController) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + files: [TelegramMediaFile], + action: @escaping (TelegramMediaFile) -> Void, + present: @escaping (ViewController) -> Void, + presentInGlobalOverlay: @escaping (ViewController) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.files = files + self.action = action + self.present = present + self.presentInGlobalOverlay = presentInGlobalOverlay + } + + static func ==(lhs: StickersResultPanelComponent, rhs: StickersResultPanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.files != rhs.files { + return false + } + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var bottomInset: CGFloat + var topInset: CGFloat + var sideInset: CGFloat + var itemSize: CGSize + var itemSpacing: CGFloat + var itemsPerRow: Int + var itemCount: Int + + var contentSize: CGSize + + init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemSize: CGSize, itemSpacing: CGFloat, itemsPerRow: Int, itemCount: Int) { + self.containerSize = containerSize + self.bottomInset = bottomInset + self.topInset = topInset + self.sideInset = sideInset + self.itemSize = itemSize + self.itemSpacing = itemSpacing + self.itemsPerRow = itemsPerRow + self.itemCount = itemCount + + self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemSize.height + bottomInset) + } + + func visibleItems(for rect: CGRect) -> Range? { + let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset) + var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemSize.height + self.itemSpacing))) + minVisibleRow = max(0, minVisibleRow) + let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemSize.height + self.itemSpacing))) + + let minVisibleIndex = minVisibleRow * self.itemsPerRow + let maxVisibleIndex = maxVisibleRow * self.itemsPerRow + self.itemsPerRow + + if maxVisibleIndex >= minVisibleIndex { + return minVisibleIndex ..< (maxVisibleIndex + 1) + } else { + return nil + } + } + + func itemFrame(for index: Int) -> CGRect { + let rowIndex = Int(floor(CGFloat(index) / CGFloat(self.itemsPerRow))) + let columnIndex = index % self.itemsPerRow + + return CGRect(origin: CGPoint(x: self.sideInset + CGFloat(columnIndex) * (self.itemSize.width + self.itemSpacing), y: self.topInset + CGFloat(rowIndex) * (self.itemSize.height + self.itemSpacing)), size: self.itemSize) + } + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private let backgroundView: BlurredBackgroundView + private let containerView: UIView + private let scrollView: UIScrollView + + private var itemLayout: ItemLayout? + + private var visibleLayers: [EngineMedia.Id: InlineStickerItemLayer] = [:] + private var fadingMaskLayer: FadingMaskLayer? + + private var ignoreScrolling = false + + private var component: StickersResultPanelComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.backgroundView.isUserInteractionEnabled = false + + self.containerView = UIView() + + self.scrollView = ScrollView() + self.scrollView.canCancelContentTouches = true + self.scrollView.delaysContentTouches = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.alwaysBounceVertical = true + self.scrollView.indicatorStyle = .white + + super.init(frame: frame) + + self.clipsToBounds = true + self.scrollView.delegate = self + + self.addSubview(self.backgroundView) + self.addSubview(self.containerView) + self.containerView.addSubview(self.scrollView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in + if let self, let component = self.component { + let presentationData = component.strings + + let convertedPoint = self.scrollView.convert(point, from: self) + guard self.scrollView.bounds.contains(convertedPoint) else { + return nil + } + + var selectedLayer: InlineStickerItemLayer? + for (_, layer) in self.visibleLayers { + if layer.frame.contains(convertedPoint) { + selectedLayer = layer + break + } + } + + if let selectedLayer, let file = selectedLayer.file { + return component.context.engine.stickers.isStickerSaved(id: file.fileId) + |> deliverOnMainQueue + |> map { [weak self] isStarred -> (UIView, CGRect, PeekControllerContent)? in + if let self, let component = self.component { + let menuItems: [ContextMenuItem] = [] + let _ = menuItems + let _ = presentationData + // if strongSelf.peerId != strongSelf.context.account.peerId && strongSelf.peerId?.namespace != Namespaces.Peer.SecretChat { + // menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + // return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + // }, action: { _, f in + // if let strongSelf = self, let peekController = strongSelf.peekController { + // if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + // let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, animationNode.view, animationNode.bounds, nil, []) + // } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + // let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, imageNode.view, imageNode.bounds, nil, []) + // } + // } + // f(.default) + // }))) + // } + // + // menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + // return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) + // }, action: { _, f in + // if let strongSelf = self, let peekController = strongSelf.peekController { + // if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + // let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, animationNode.view, animationNode.bounds, nil, []) + // } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + // let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, imageNode.view, imageNode.bounds, nil, []) + // } + // } + // f(.default) + // }))) + +// menuItems.append( +// .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in +// f(.default) +// +// if let self, let component = self.component { +// let _ = (component.context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) +// |> deliverOnMainQueue).start(next: { [weak self] result in +// guard let self, let component = self.component else { +// return +// } +// switch result { +// case .generic: +// let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: file, loop: true, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }) +// component.presentInGlobalOverlay(controller) +// case let .limitExceeded(limit, premiumLimit): +// let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) +// let text: String +// if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { +// text = presentationData.strings.Premium_MaxFavedStickersFinalText +// } else { +// text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string +// } +// +// let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: file, loop: true, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in +// if let self, let component = self.component { +// if case .info = action { +// let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .savedStickers) +// // strongSelf.getControllerInteraction?()?.navigationController()?.pushViewController(controller) +// return true +// } +// } +// return false +// }) +// component.presentInGlobalOverlay(controller) +// } +// }) +// } +// })) +// ) +// +// menuItems.append( +// .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in +// f(.default) +// +// if let self, let component = self.component { +// loop: for attribute in file.attributes { +// switch attribute { +// case let .Sticker(_, packReference, _): +// if let packReference = packReference { +// let controller = component.context.sharedContext.makeStickerPackScreen(context: component.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: nil, sendSticker: { [weak self] file, sourceNode, sourceRect in +// if let self, let component = self.component { +// component.action(file) +// return true +// } else { +// return false +// } +// }) +// component.present(controller) +// } +// break loop +// default: +// break +// } +// } +// } +// })) +// ) + return (self, selectedLayer.frame, StickerPreviewPeekContent(context: component.context, theme: component.theme, strings: component.strings, item: .pack(file), menu: menuItems, openPremiumIntro: { [weak self] in + guard let self, let component = self.component else { + return + } + let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .stickers) + component.present(controller) + // let controller = PremiumIntroScreen(context: component.context, source: .stickers) + // controllerInteraction.navigationController()?.pushViewController(controller) + })) + } else { + return nil + } + } + } + } + return nil + }, present: { [weak self] content, sourceView, sourceRect in + if let self, let component = self.component { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceView: { + return (sourceView, sourceRect) + }) + // controller.visibilityUpdated = { [weak self] visible in + // self?.previewingStickersPromise.set(visible) + // } + component.presentInGlobalOverlay(controller) + // strongSelf.peekController = controller + // strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(controller, nil) + return controller + } + return nil + }, updateContent: { [weak self] content in + if let self { + var item: TelegramMediaFile? + if let content = content as? StickerPreviewPeekContent, case let .pack(contentItem) = content.item { + item = contentItem + } + let _ = item + let _ = self + //strongSelf.updatePreviewingItem(file: item, animated: true) + } + }) + self.addGestureRecognizer(peekRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let location = recognizer.location(in: self.scrollView) + if self.scrollView.bounds.contains(location) { + var closestFile: (file: TelegramMediaFile, distance: CGFloat)? + for (_, itemLayer) in self.visibleLayers { + guard let file = itemLayer.file else { + continue + } + if itemLayer.frame.contains(location) { + closestFile = (file, 0.0) + } + } + if let (file, _) = closestFile { + self.component?.action(file) + } + } + } + } + + func animateIn(transition: Transition) { + let offset = self.scrollView.contentOffset.y * -1.0 + 10.0 + Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset)) + transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: 0.0)) + } + + func animateOut(transition: Transition, completion: @escaping () -> Void) { + let offset = self.scrollView.contentOffset.y * -1.0 + 10.0 + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset), completion: { _ in + completion() + }) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + private func updateScrolling(transition: Transition) { + guard let component = self.component, let itemLayout = self.itemLayout else { + return + } + + let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0) + + var synchronousLoad = false + if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) { + synchronousLoad = hint.synchronousLoad + } + + var visibleIds = Set() + if let range = itemLayout.visibleItems(for: visibleBounds) { + for index in range.lowerBound ..< range.upperBound { + guard index < component.files.count else { + continue + } + + let itemFrame = itemLayout.itemFrame(for: index) + + let item = component.files[index] + visibleIds.insert(item.fileId) + + let itemLayer: InlineStickerItemLayer + if let current = self.visibleLayers[item.fileId] { + itemLayer = current + itemLayer.dynamicColor = .white + } else { + itemLayer = InlineStickerItemLayer( + context: component.context, + userLocation: .other, + attemptSynchronousLoad: synchronousLoad, + emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: item.fileId.id, file: item), + file: item, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + placeholderColor: UIColor(rgb: 0xffffff).mixedWith(UIColor(rgb: 0x1c1c1d), alpha: 0.9), + pointSize: itemFrame.size, + dynamicColor: .white + ) + self.visibleLayers[item.fileId] = itemLayer + self.scrollView.layer.addSublayer(itemLayer) + } + + itemLayer.frame = itemFrame + + itemLayer.isVisibleForAnimations = true + } + } + + var removedIds: [EngineMedia.Id] = [] + for (id, itemLayer) in self.visibleLayers { + if !visibleIds.contains(id) { + itemLayer.removeFromSuperlayer() + removedIds.append(id) + } + } + for id in removedIds { + self.visibleLayers.removeValue(forKey: id) + } + + let backgroundSize = CGSize(width: self.scrollView.frame.width, height: self.scrollView.frame.height + 20.0) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(0.0, self.scrollView.contentOffset.y * -1.0)), size: backgroundSize)) + self.backgroundView.update(size: backgroundSize, cornerRadius: 11.0, transition: transition.containedViewLayoutTransition) + } + + func update(component: StickersResultPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + //let itemUpdated = self.component?.results != component.results + + self.component = component + self.state = state + + let minimizedHeight = min(availableSize.height, 500.0) + + self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition) + + let itemsPerRow = min(8, max(5, Int(availableSize.width / 80))) + let sideInset: CGFloat = 2.0 + let itemSpacing: CGFloat = 2.0 + let itemSize = floor((availableSize.width - sideInset * 2.0 - itemSpacing * (CGFloat(itemsPerRow) - 1.0)) / CGFloat(itemsPerRow)) + + let itemLayout = ItemLayout( + containerSize: CGSize(width: availableSize.width, height: minimizedHeight), + bottomInset: 9.0, + topInset: 9.0, + sideInset: sideInset, + itemSize: CGSize(width: itemSize, height: itemSize), + itemSpacing: itemSpacing, + itemsPerRow: itemsPerRow, + itemCount: component.files.count + ) + self.itemLayout = itemLayout + + let scrollContentSize = itemLayout.contentSize + + self.ignoreScrolling = true + + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: minimizedHeight))) + + let visibleTopContentHeight = min(scrollContentSize.height, itemSize * 3.0 + 19.0) + let topInset = availableSize.height - visibleTopContentHeight + + let scrollContentInsets = UIEdgeInsets(top: topInset, left: 0.0, bottom: 19.0, right: 0.0) + let scrollIndicatorInsets = UIEdgeInsets(top: topInset + 17.0, left: 0.0, bottom: 19.0, right: 0.0) + if self.scrollView.contentInset != scrollContentInsets { + self.scrollView.contentInset = scrollContentInsets + } + if self.scrollView.scrollIndicatorInsets != scrollIndicatorInsets { + self.scrollView.scrollIndicatorInsets = scrollIndicatorInsets + } + if self.scrollView.contentSize != scrollContentSize { + self.scrollView.contentSize = scrollContentSize + } + + let maskLayer: FadingMaskLayer + if let current = self.fadingMaskLayer { + maskLayer = current + } else { + maskLayer = FadingMaskLayer() + self.fadingMaskLayer = maskLayer + } + if self.containerView.layer.mask == nil { + self.containerView.layer.mask = maskLayer + } + maskLayer.frame = CGRect(origin: .zero, size: self.scrollView.frame.size) + + self.containerView.frame = CGRect(origin: .zero, size: availableSize) + + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class FadingMaskLayer: SimpleLayer { + let gradientLayer = SimpleLayer() + let fillLayer = SimpleLayer() + + override func layoutSublayers() { + let gradientHeight: CGFloat = 110.0 + if self.gradientLayer.contents == nil { + self.addSublayer(self.gradientLayer) + self.addSublayer(self.fillLayer) + + let gradientImage = generateGradientImage(size: CGSize(width: 1.0, height: gradientHeight), colors: [UIColor.white, UIColor.white, UIColor.white.withAlphaComponent(0.0), UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 0.4, 0.9, 1.0], direction: .vertical) + self.gradientLayer.contents = gradientImage?.cgImage + self.gradientLayer.contentsGravity = .resize + self.fillLayer.backgroundColor = UIColor.white.cgColor + } + + self.fillLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height - gradientHeight)) + self.gradientLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.height - gradientHeight), size: CGSize(width: self.bounds.width, height: gradientHeight)) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 114fcc631b..31f452f277 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -71,6 +71,7 @@ swift_library( "//submodules/Components/BundleIconComponent", "//submodules/TinyThumbnail", "//submodules/ImageBlur", + "//submodules/StickerPackPreviewUI", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 78c3cb5cad..7dc185ed17 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -688,7 +688,7 @@ private final class StoryContainerScreenComponent: Component { chatPeerId: nil, areCustomEmojiEnabled: true, hasTrending: false, - hasSearch: false, + hasSearch: true, hideBackground: true, sendGif: nil ) @@ -847,7 +847,7 @@ private final class StoryContainerScreenComponent: Component { var itemSetContainerSafeInsets = environment.safeInsets if case .regular = environment.metrics.widthClass { let availableHeight = min(1080.0, availableSize.height - max(45.0, environment.safeInsets.bottom) * 2.0) - let mediaHeight = availableHeight - 40.0 + let mediaHeight = availableHeight - 60.0 let mediaWidth = floor(mediaHeight * 0.5625) itemSetContainerSize = CGSize(width: mediaWidth, height: availableHeight) itemSetContainerInsets.top = 0.0 @@ -886,6 +886,12 @@ private final class StoryContainerScreenComponent: Component { environment.controller()?.present(c, in: .window(.root), with: a) } }, + presentInGlobalOverlay: { [weak self] c, a in + guard let self, let environment = self.environment else { + return + } + environment.controller()?.presentInGlobalOverlay(c, with: a) + }, close: { [weak self] in guard let self, let environment = self.environment else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 2946b3ae2f..7e92323b2e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -91,6 +91,7 @@ public final class StoryItemSetContainerComponent: Component { public let verticalPanFraction: CGFloat public let pinchState: PinchState? public let presentController: (ViewController, Any?) -> Void + public let presentInGlobalOverlay: (ViewController, Any?) -> Void public let close: () -> Void public let navigate: (NavigationDirection) -> Void public let delete: () -> Void @@ -120,6 +121,7 @@ public final class StoryItemSetContainerComponent: Component { verticalPanFraction: CGFloat, pinchState: PinchState?, presentController: @escaping (ViewController, Any?) -> Void, + presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, close: @escaping () -> Void, navigate: @escaping (NavigationDirection) -> Void, delete: @escaping () -> Void, @@ -148,6 +150,7 @@ public final class StoryItemSetContainerComponent: Component { self.verticalPanFraction = verticalPanFraction self.pinchState = pinchState self.presentController = presentController + self.presentInGlobalOverlay = presentInGlobalOverlay self.close = close self.navigate = navigate self.delete = delete @@ -355,6 +358,7 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext = StoryItemSetContainerSendMessage() self.itemsContainerView = UIView() + //self.itemsContainerView.clipsToBounds = true self.scroller = Scroller() self.scroller.alwaysBounceHorizontal = true @@ -380,9 +384,7 @@ public final class StoryItemSetContainerComponent: Component { self.transitionCloneContainerView = UIView() super.init(frame: frame) - - self.clipsToBounds = true - + self.itemsContainerView.addSubview(self.scroller) self.scroller.delegate = self self.itemsContainerView.addGestureRecognizer(self.scroller.panGestureRecognizer) @@ -581,6 +583,9 @@ public final class StoryItemSetContainerComponent: Component { if hasFirstResponder(self) { self.sendMessageContext.currentInputMode = .text self.endEditing(true) + } else if case .media = self.sendMessageContext.currentInputMode { + self.sendMessageContext.currentInputMode = .text + self.state?.updated(transition: .spring(duration: 0.4)) } else if self.displayViewList { let point = recognizer.location(in: self) @@ -1425,9 +1430,9 @@ public final class StoryItemSetContainerComponent: Component { func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let isFirstTime = self.component == nil - if let hint = transition.userData(TextFieldComponent.AnimationHint.self), case .textFocusChanged = hint.kind, !hasFirstResponder(self) { - self.sendMessageContext.currentInputMode = .text - } +// if let hint = transition.userData(TextFieldComponent.AnimationHint.self), case .textFocusChanged = hint.kind, !hasFirstResponder(self) { +// self.sendMessageContext.currentInputMode = .text +// } if self.component == nil { self.sendMessageContext.setup(context: component.context, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData) @@ -1517,6 +1522,7 @@ public final class StoryItemSetContainerComponent: Component { disabledPlaceholder = "You can't reply to this story" } + var keyboardHeight = component.deviceMetrics.standardInputHeight(inLandscape: false) let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden let inputNodeVisible = self.sendMessageContext.currentInputMode == .media || hasFirstResponder(self) self.inputPanel.parentState = state @@ -1544,12 +1550,24 @@ public final class StoryItemSetContainerComponent: Component { } component.presentController(c, nil) }, + presentInGlobalOverlay: { [weak self] c in + guard let self, let component = self.component else { + return + } + component.presentInGlobalOverlay(c, nil) + }, sendMessageAction: { [weak self] in guard let self else { return } self.sendMessageContext.performSendMessageAction(view: self) }, + sendStickerAction: { [weak self] sticker in + guard let self else { + return + } + self.sendMessageContext.performSendStickerAction(view: self, fileReference: .standalone(media: sticker)) + }, setMediaRecordingActive: { [weak self] isActive, isVideo, sendAction in guard let self else { return @@ -1634,6 +1652,7 @@ public final class StoryItemSetContainerComponent: Component { displayGradient: false, //(component.inputHeight != 0.0 || inputNodeVisible) && component.metrics.widthClass != .regular, bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset, hideKeyboard: self.sendMessageContext.currentInputMode == .media, + forceIsEditing: self.sendMessageContext.currentInputMode == .media, disabledPlaceholder: disabledPlaceholder )), environment: {}, @@ -1646,12 +1665,18 @@ public final class StoryItemSetContainerComponent: Component { inputHeight = component.deviceMetrics.standardInputHeight(inLandscape: false) } } + + let inputMediaNodeHeight = self.sendMessageContext.updateInputMediaNode(inputPanel: self.inputPanel, availableSize: availableSize, bottomInset: component.safeInsets.bottom, inputHeight: component.inputHeight, effectiveInputHeight: inputHeight, metrics: component.metrics, deviceMetrics: component.deviceMetrics, transition: transition) + if inputMediaNodeHeight > 0.0 { + inputHeight = inputMediaNodeHeight + } + keyboardHeight = max(keyboardHeight, inputMediaNodeHeight) let inputPanelBackgroundSize = self.inputPanelBackground.update( transition: transition, component: AnyComponent(BlurredGradientComponent(position: .bottom, dark: true, tag: nil)), environment: {}, - containerSize: CGSize(width: availableSize.width, height: component.deviceMetrics.standardInputHeight(inLandscape: false) + 100.0) + containerSize: CGSize(width: availableSize.width, height: keyboardHeight + 100.0) ) if let inputPanelBackgroundView = self.inputPanelBackground.view { if inputPanelBackgroundView.superview == nil { @@ -1662,8 +1687,6 @@ public final class StoryItemSetContainerComponent: Component { transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4) } - self.sendMessageContext.updateInputMediaNode(inputPanel: self.inputPanel, availableSize: availableSize, bottomInset: component.safeInsets.bottom, inputHeight: component.inputHeight, effectiveInputHeight: inputHeight, metrics: component.metrics, deviceMetrics: component.deviceMetrics, transition: transition) - var viewListInset: CGFloat = 0.0 var inputPanelBottomInset: CGFloat @@ -2295,9 +2318,9 @@ public final class StoryItemSetContainerComponent: Component { if self.voiceMessagesRestrictedTooltipController != nil { effectiveDisplayReactions = false } -// if self.sendMessageContext.currentInputMode != .text { -// effectiveDisplayReactions = false -// } + if self.sendMessageContext.currentInputMode != .text { + effectiveDisplayReactions = false + } if let reactionContextNode = self.reactionContextNode, reactionContextNode.isReactionSearchActive { effectiveDisplayReactions = true @@ -2530,7 +2553,8 @@ public final class StoryItemSetContainerComponent: Component { reactionContextNode.animateOut(to: reactionsAnchorRect, animatingOutToReaction: true) } } else { - transition.setAlpha(view: reactionContextNode.view, alpha: 0.0, completion: { [weak reactionContextNode] _ in + let reactionTransition = Transition.easeInOut(duration: 0.25) + reactionTransition.setAlpha(view: reactionContextNode.view, alpha: 0.0, completion: { [weak reactionContextNode] _ in reactionContextNode?.view.removeFromSuperview() }) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 15fa5e7648..9b972787eb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -35,6 +35,7 @@ import Postbox import OverlayStatusController import PresentationDataUtils import TextFieldComponent +import StickerPackPreviewUI final class StoryItemSetContainerSendMessage { enum InputMode { @@ -127,7 +128,11 @@ final class StoryItemSetContainerSendMessage { switchToTextInput: { [weak self] in if let self { self.currentInputMode = .text - self.view?.state?.updated(transition: .immediate) + if let view = self.view, !hasFirstResponder(view) { + let _ = view.activateInput() + } else { + self.view?.state?.updated(transition: .immediate) + } } }, dismissTextInput: { @@ -156,9 +161,9 @@ final class StoryItemSetContainerSendMessage { getNavigationController: { return self.view?.component?.controller()?.navigationController as? NavigationController }, - requestLayout: { [weak self] _ in + requestLayout: { [weak self] transition in if let self { - self.view?.state?.updated() + self.view?.state?.updated(transition: Transition(transition)) } } ) @@ -179,11 +184,12 @@ final class StoryItemSetContainerSendMessage { } } - func updateInputMediaNode(inputPanel: ComponentView, availableSize: CGSize, bottomInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: Transition) { + func updateInputMediaNode(inputPanel: ComponentView, availableSize: CGSize, bottomInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: Transition) -> CGFloat { guard let context = self.context, let inputPanelView = inputPanel.view as? MessageInputPanelComponent.View else { - return + return 0.0 } - + + var height: CGFloat = 0.0 if let component = self.view?.component, case .media = self.currentInputMode, let inputData = self.inputMediaNodeData { let inputMediaNode: ChatEntityKeyboardInputNode if let current = self.inputMediaNode { @@ -200,10 +206,10 @@ final class StoryItemSetContainerSendMessage { stateContext: self.inputMediaNodeStateContext ) inputMediaNode.externalTopPanelContainerImpl = nil + inputMediaNode.useExternalSearchContainer = true if inputMediaNode.view.superview == nil { self.inputMediaNodeBackground.removeAllAnimations() self.inputMediaNodeBackground.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.7).cgColor -// inputPanelView.superview?.layer.insertSublayer(self.inputMediaNodeBackground, below: inputPanelView.layer) inputPanelView.superview?.insertSubview(inputMediaNode.view, belowSubview: inputPanelView) } self.inputMediaNode = inputMediaNode @@ -241,6 +247,8 @@ final class StoryItemSetContainerSendMessage { } transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame) + + height = heightAndOverflow.0 } else if let inputMediaNode = self.inputMediaNode { self.inputMediaNode = nil @@ -279,6 +287,8 @@ final class StoryItemSetContainerSendMessage { inputPanelView.activateInput() } } + + return height } func animateOut(bounds: CGRect) { @@ -317,7 +327,25 @@ final class StoryItemSetContainerSendMessage { let peer = component.slice.peer let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let controller = component.controller() + let controller = component.controller() as? StoryContainerScreen + + if let navigationController = controller?.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + for controller in controllers.reversed() { + if !(controller is StoryContainerScreen) { + controllers.removeLast() + } else { + break + } + } + navigationController.setViewControllers(controllers, animated: true) + + controller?.window?.forEachController({ controller in + if let controller = controller as? StickerPackScreenImpl { + controller.dismiss() + } + }) + } let _ = (component.context.engine.messages.enqueueOutgoingMessage( to: peerId, @@ -344,7 +372,12 @@ final class StoryItemSetContainerSendMessage { }) self.currentInputMode = .text - view.endEditing(true) + if hasFirstResponder(view) { + view.endEditing(true) + } else { + view.state?.updated(transition: .spring(duration: 0.3)) + controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) + } } func performSendGifAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) { diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 96a9a25363..d5f2dde083 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -59,7 +59,7 @@ public final class TextFieldComponent: Component { public let kind: Kind - fileprivate init(kind: Kind) { + public init(kind: Kind) { self.kind = kind } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 0168ad77c0..eb061ba44d 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -57,26 +57,6 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation return updates } -struct StickersSearchConfiguration { - static var defaultValue: StickersSearchConfiguration { - return StickersSearchConfiguration(disableLocalSuggestions: false) - } - - public let disableLocalSuggestions: Bool - - fileprivate init(disableLocalSuggestions: Bool) { - self.disableLocalSuggestions = disableLocalSuggestions - } - - static func with(appConfiguration: AppConfiguration) -> StickersSearchConfiguration { - if let data = appConfiguration.data, let suggestOnlyApi = data["stickers_emoji_suggest_only_api"] as? Bool { - return StickersSearchConfiguration(disableLocalSuggestions: suggestOnlyApi) - } else { - return .defaultValue - } - } -} - private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer, chatLocation: ChatLocation, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?, requestBotLocationStatus: @escaping (PeerId) -> Void) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> { switch inputQuery { case let .emoji(query):