From 3b42f146137cfa0765444306a68f0262f12886d3 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 23 Jan 2023 14:36:47 +0400 Subject: [PATCH] UI improvements --- .../Source/Base/Transition.swift | 2 +- .../Sources/PagerComponent.swift | 15 +- .../Sources/StickerPickerScreen.swift | 6 +- .../Sources/MTRequestMessageService.m | 2 + .../Sources/ReactionContextNode.swift | 4 +- .../Sources/AvatarEditorScreen.swift | 4 +- .../Sources/ChatEntityKeyboardInputNode.swift | 94 +++- .../EmojiStatusSelectionComponent.swift | 4 +- .../Sources/EmojiPagerContentComponent.swift | 147 ++++-- .../Sources/EmojiSearchContent.swift | 466 ++++++++++++++++++ .../EmojiSearchSearchBarComponent.swift | 48 +- .../Sources/EntityKeyboard.swift | 30 +- .../Sources/GifPagerContentComponent.swift | 28 +- .../Sources/ForumCreateTopicScreen.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 97 ++-- .../Sources/ChatTextInputPanelNode.swift | 2 +- .../EmojisChatInputContextPanelNode.swift | 283 +++++++++++ .../Sources/EmojisChatInputPanelItem.swift | 6 +- 18 files changed, 1085 insertions(+), 155 deletions(-) create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 4fa4a7aada..f8f6e6344b 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -150,7 +150,7 @@ public struct Transition { //view.center = CGPoint(x: frame.midX, y: frame.midY) self.animatePosition(view: view, from: CGPoint(x: previousFrame.midX, y: previousFrame.midY), to: CGPoint(x: frame.midX, y: frame.midY), completion: completion) - self.animateBounds(view: view, from: CGRect(origin: view.bounds.origin, size: previousFrame.size), to: CGRect(origin: view.bounds.origin, size: frame.size)) + self.animateBoundsSize(view: view, from: previousFrame.size, to: frame.size) } } diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index 8d6e0c57fa..d5ecbd7337 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -149,6 +149,7 @@ public enum PagerComponentPanelHideBehavior { case hideOnScroll case show case hide + case disable } public final class PagerComponentContentIcon: Equatable { @@ -559,6 +560,10 @@ public final class PagerComponent take(1) |> deliverOnMainQueue).start(next: { hasPremium in + let _ = ( + combineLatest( + ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), + ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false) + ) + |> take(1) + |> deliverOnMainQueue).start(next: { hasPremium, hasGlobalPremium in guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else { return } if groupId == AnyHashable("featuredTop"), let file = item.itemFile { let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) - let _ = (context.account.postbox.combinedView(keys: [viewKey]) + let _ = (combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + context.account.postbox.combinedView(keys: [viewKey]) + ) |> take(1) - |> deliverOnMainQueue).start(next: { [weak interfaceInteraction, weak controllerInteraction] views in + |> deliverOnMainQueue).start(next: { [weak interfaceInteraction, weak controllerInteraction] emojiPacksView, views in guard let controllerInteraction = controllerInteraction else { return } guard let view = views.views[viewKey] as? OrderedItemListView else { return } - for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + guard let self else { + return + } + + let _ = interfaceInteraction + let _ = controllerInteraction + + var installedCollectionIds = Set() + for (id, _, _) in emojiPacksView.collectionInfos { + installedCollectionIds.insert(id) + } + + let stickerPacks = view.items.map({ $0.contents.get(FeaturedStickerPackItem.self)! }).filter({ + !installedCollectionIds.contains($0.info.id) + }) + + for featuredStickerPack in stickerPacks { if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) { - let controller = StickerPackScreen( + if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiInputInteraction { + pagerView.openCustomSearch(content: EmojiSearchContent( + context: self.context, + items: stickerPacks, + initialFocusId: featuredStickerPack.info.id, + hasPremiumForUse: hasPremium, + hasPremiumForInstallation: hasGlobalPremium, + parentInputInteraction: emojiInputInteraction + )) + } + + /*let controller = StickerPackScreen( context: context, updatedPresentationData: controllerInteraction.updatedPresentationData, mode: .default, @@ -675,7 +711,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) } ) - controllerInteraction.presentController(controller, nil) + controllerInteraction.presentController(controller, nil)*/ break } @@ -786,7 +822,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }, openSearch: { }, - addGroupAction: { [weak self, weak controllerInteraction] groupId, isPremiumLocked in + addGroupAction: { [weak self, weak controllerInteraction] groupId, isPremiumLocked, scrollToGroup in guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else { return } @@ -815,7 +851,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { if featuredEmojiPack.info.id == collectionId { if let strongSelf = self { - strongSelf.scheduledContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) + strongSelf.scheduledContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: scrollToGroup)) } let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() @@ -1181,7 +1217,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { pagerView.openSearch() } }, - addGroupAction: { groupId, isPremiumLocked in + addGroupAction: { groupId, isPremiumLocked, _ in guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else { return } @@ -1479,7 +1515,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return } - strongSelf.openGifContextMenu(item: item, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) + strongSelf.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) }, loadMore: { [weak self] token in guard let strongSelf = self, let gifContext = strongSelf.gifContext else { @@ -1697,7 +1733,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } strongSelf.reorderItems(category: category, items: items) }, - makeSearchContainerNode: { [weak controllerInteraction] content in + makeSearchContainerNode: { [weak self, weak controllerInteraction] content in guard let controllerInteraction = controllerInteraction else { return nil } @@ -1711,7 +1747,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - return PaneSearchContainerNode( + let searchContainerNode = PaneSearchContainerNode( context: context, theme: presentationData.theme, strings: presentationData.strings, @@ -1722,6 +1758,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { cancel: { } ) + searchContainerNode.openGifContextMenu = { item, sourceNode, sourceRect, gesture, isSaved in + guard let self else { + return + } + self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) + } + + return searchContainerNode }, contentIdUpdated: { _ in }, deviceMetrics: deviceMetrics, @@ -1854,17 +1898,15 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }) } - private func openGifContextMenu(item: GifPagerContentComponent.Item, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { - let file = item - + private func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { let canSaveGif: Bool - if file.file.media.fileId.namespace == Namespaces.Media.CloudFile { + if file.media.fileId.namespace == Namespaces.Media.CloudFile { canSaveGif = true } else { canSaveGif = false } - let _ = (self.context.engine.stickers.isGifSaved(id: file.file.media.fileId) + let _ = (self.context.engine.stickers.isGifSaved(id: file.media.fileId) |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in guard let strongSelf = self else { return @@ -1875,7 +1917,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in }, baseNavigationController: nil) @@ -1887,8 +1929,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }, action: { _, f in f(.default) if isSaved { - let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, false, false) - } else if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, false, false) + } else if let (collection, result) = contextResult { let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false) } }))) @@ -1908,8 +1950,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }, action: { _, f in f(.default) if isSaved { - let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, true, false) - } else if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, true, false) + } else if let (collection, result) = contextResult { let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, true) } }))) @@ -1921,7 +1963,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }, action: { _, f in f(.default) - let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, false, true) + let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, false, true) }))) } } @@ -1937,7 +1979,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return } - let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.file.media.fileId).start() + let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.media.fileId).start() }))) } else if canSaveGif && !isGifSaved { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in @@ -1951,7 +1993,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let context = strongSelf.context let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let _ = (toggleGifSaved(account: context.account, fileReference: file.file, saved: true) + let _ = (toggleGifSaved(account: context.account, fileReference: file, saved: true) |> deliverOnMainQueue).start(next: { result in guard let strongSelf = self else { return @@ -2136,7 +2178,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi }, openSearch: { }, - addGroupAction: { _, _ in + addGroupAction: { _, _, _ in }, clearGroup: { [weak self] groupId in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index fd1d470605..3505d92000 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -395,7 +395,7 @@ public final class EmojiStatusSelectionController: ViewController { }, openSearch: { }, - addGroupAction: { groupId, isPremiumLocked in + addGroupAction: { groupId, isPremiumLocked, _ in guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else { return } @@ -410,7 +410,7 @@ public final class EmojiStatusSelectionController: ViewController { for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { if featuredEmojiPack.info.id == collectionId { if let strongSelf = self { - strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) + strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: true)) } let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 8d719df9ef..8cd841e38d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1892,7 +1892,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { component: AnyComponent(Text( text: strings.Common_Cancel, font: Font.regular(17.0), - color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + color: useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor )), environment: {}, containerSize: CGSize(width: size.width - 32.0, height: 100.0) @@ -2178,7 +2178,7 @@ public final class EmojiPagerContentComponent: Component { public enum AnimationType { case generic case groupExpanded(id: AnyHashable) - case groupInstalled(id: AnyHashable) + case groupInstalled(id: AnyHashable, scrollToGroup: Bool) case groupRemoved(id: AnyHashable) } @@ -2239,7 +2239,7 @@ public final class EmojiPagerContentComponent: Component { public let openStickerSettings: (() -> Void)? public let openFeatured: (() -> Void)? public let openSearch: () -> Void - public let addGroupAction: (AnyHashable, Bool) -> Void + public let addGroupAction: (AnyHashable, Bool, Bool) -> Void public let clearGroup: (AnyHashable) -> Void public let pushController: (ViewController) -> Void public let presentController: (ViewController) -> Void @@ -2248,6 +2248,7 @@ public final class EmojiPagerContentComponent: Component { public let requestUpdate: (Transition) -> Void public let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void public let updateScrollingToItemGroup: () -> Void + public let externalCancel: (() -> Void)? public let chatPeerId: PeerId? public let peekBehavior: EmojiContentPeekBehavior? public let customLayout: CustomLayout? @@ -2262,7 +2263,7 @@ public final class EmojiPagerContentComponent: Component { openStickerSettings: (() -> Void)?, openFeatured: (() -> Void)?, openSearch: @escaping () -> Void, - addGroupAction: @escaping (AnyHashable, Bool) -> Void, + addGroupAction: @escaping (AnyHashable, Bool, Bool) -> Void, clearGroup: @escaping (AnyHashable) -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, @@ -2271,6 +2272,7 @@ public final class EmojiPagerContentComponent: Component { requestUpdate: @escaping (Transition) -> Void, updateSearchQuery: @escaping (SearchQuery?) -> Void, updateScrollingToItemGroup: @escaping () -> Void, + externalCancel: (() -> Void)? = nil, chatPeerId: PeerId?, peekBehavior: EmojiContentPeekBehavior?, customLayout: CustomLayout?, @@ -2293,6 +2295,7 @@ public final class EmojiPagerContentComponent: Component { self.requestUpdate = requestUpdate self.updateSearchQuery = updateSearchQuery self.updateScrollingToItemGroup = updateScrollingToItemGroup + self.externalCancel = externalCancel self.chatPeerId = chatPeerId self.peekBehavior = peekBehavior self.customLayout = customLayout @@ -2541,6 +2544,7 @@ public final class EmojiPagerContentComponent: Component { public let displaySearchWithPlaceholder: String? public let searchCategories: EmojiSearchCategories? public let searchInitiallyHidden: Bool + public let searchAlwaysActive: Bool public let searchIsPlaceholderOnly: Bool public let emptySearchResults: EmptySearchResults? public let enableLongPress: Bool @@ -2561,6 +2565,7 @@ public final class EmojiPagerContentComponent: Component { displaySearchWithPlaceholder: String?, searchCategories: EmojiSearchCategories?, searchInitiallyHidden: Bool, + searchAlwaysActive: Bool, searchIsPlaceholderOnly: Bool, emptySearchResults: EmptySearchResults?, enableLongPress: Bool, @@ -2580,6 +2585,7 @@ public final class EmojiPagerContentComponent: Component { self.displaySearchWithPlaceholder = displaySearchWithPlaceholder self.searchCategories = searchCategories self.searchInitiallyHidden = searchInitiallyHidden + self.searchAlwaysActive = searchAlwaysActive self.searchIsPlaceholderOnly = searchIsPlaceholderOnly self.emptySearchResults = emptySearchResults self.enableLongPress = enableLongPress @@ -2602,6 +2608,7 @@ public final class EmojiPagerContentComponent: Component { displaySearchWithPlaceholder: self.displaySearchWithPlaceholder, searchCategories: self.searchCategories, searchInitiallyHidden: self.searchInitiallyHidden, + searchAlwaysActive: self.searchAlwaysActive, searchIsPlaceholderOnly: self.searchIsPlaceholderOnly, emptySearchResults: emptySearchResults, enableLongPress: self.enableLongPress, @@ -4296,7 +4303,7 @@ public final class EmojiPagerContentComponent: Component { } } - public func scrollToItemGroup(id supergroupId: AnyHashable, subgroupId: Int32?) { + public func scrollToItemGroup(id supergroupId: AnyHashable, subgroupId: Int32?, animated: Bool) { guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else { return } @@ -4332,6 +4339,9 @@ public final class EmojiPagerContentComponent: Component { } var scrollPosition = anchorFrame.minY + floor(-itemLayout.verticalGroupDefaultSpacing / 2.0) - pagerEnvironment.containerInsets.top + if !animated { + scrollPosition = floor(anchorFrame.midY - self.scrollView.bounds.height * 0.5) + } if scrollPosition > self.scrollView.contentSize.height - self.scrollView.bounds.height { scrollPosition = self.scrollView.contentSize.height - self.scrollView.bounds.height } @@ -4339,6 +4349,26 @@ public final class EmojiPagerContentComponent: Component { scrollPosition = 0.0 } + if !animated, let keyboardChildEnvironment = self.keyboardChildEnvironment, let inputInteraction = component.inputInteractionHolder.inputInteraction, inputInteraction.useOpaqueTheme { + let highlightLayer = SimpleLayer() + highlightLayer.backgroundColor = keyboardChildEnvironment.theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor + highlightLayer.cornerRadius = 20.0 + var highlightFrame = group.frame + if highlightFrame.origin.x < 4.0 { + highlightFrame.size.width += (4.0 - highlightFrame.origin.x) + highlightFrame.origin.x = 4.0 + } + if highlightFrame.minX + highlightFrame.size.width > self.scrollView.bounds.width - 4.0 { + highlightFrame.size.width = self.scrollView.bounds.width - 4.0 - highlightFrame.minX + } + + highlightLayer.frame = highlightFrame + self.scrollView.layer.insertSublayer(highlightLayer, at: 0) + highlightLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.8, removeOnCompletion: false, completion: { [weak highlightLayer] _ in + highlightLayer?.removeFromSuperlayer() + }) + } + let offsetDirectionSign: Double = scrollPosition < self.scrollView.bounds.minY ? -1.0 : 1.0 var previousVisibleLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:] @@ -4462,7 +4492,7 @@ public final class EmojiPagerContentComponent: Component { } } - let duration = 0.4 + let duration: Double = animated ? 0.4 : 0.0 let timingFunction = kCAMediaTimingFunctionSpring if let commonItemOffset = commonItemOffset { @@ -4779,41 +4809,43 @@ public final class EmojiPagerContentComponent: Component { return } if case .ended = recognizer.state { - let locationInScrollView = recognizer.location(in: self.scrollView) - outer: for (id, groupHeader) in self.visibleGroupHeaders { - if groupHeader.frame.insetBy(dx: -10.0, dy: -6.0).contains(locationInScrollView) { - let groupHeaderPoint = self.scrollView.convert(locationInScrollView, to: groupHeader) - if let clearIconLayer = groupHeader.clearIconLayer, clearIconLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(groupHeaderPoint) { - component.inputInteractionHolder.inputInteraction?.clearGroup(id) - return - } else { - if groupHeader.tapGesture(point: recognizer.location(in: groupHeader)) { + if self.scrollViewClippingView.bounds.contains(recognizer.location(in: self.scrollViewClippingView)) { + let locationInScrollView = recognizer.location(in: self.scrollView) + outer: for (id, groupHeader) in self.visibleGroupHeaders { + if groupHeader.frame.insetBy(dx: -10.0, dy: -6.0).contains(locationInScrollView) { + let groupHeaderPoint = self.scrollView.convert(locationInScrollView, to: groupHeader) + if let clearIconLayer = groupHeader.clearIconLayer, clearIconLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(groupHeaderPoint) { + component.inputInteractionHolder.inputInteraction?.clearGroup(id) return + } else { + if groupHeader.tapGesture(point: recognizer.location(in: groupHeader)) { + return + } } } } - } - - var foundItem = false - var foundExactItem = false - if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] { - foundExactItem = true - foundItem = true - if !itemLayer.displayPlaceholder { - component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false) - } - } - - if !foundExactItem { - if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self), extendedHitRange: true), let itemLayer = self.visibleItemLayers[itemKey] { + + var foundItem = false + var foundExactItem = false + if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] { + foundExactItem = true foundItem = true if !itemLayer.displayPlaceholder { component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false) } } + + if !foundExactItem { + if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self), extendedHitRange: true), let itemLayer = self.visibleItemLayers[itemKey] { + foundItem = true + if !itemLayer.displayPlaceholder { + component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false) + } + } + } + + let _ = foundItem } - - let _ = foundItem } } @@ -5006,7 +5038,7 @@ public final class EmojiPagerContentComponent: Component { scrollView.layer.removeAllAnimations() } - if self.isSearchActivated, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil { + if self.isSearchActivated, let component = self.component, !component.searchAlwaysActive, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil { scrollView.isScrollEnabled = false DispatchQueue.main.async { scrollView.isScrollEnabled = true @@ -5118,8 +5150,10 @@ public final class EmojiPagerContentComponent: Component { var transitionHintExpandedGroupId: AnyHashable? if let contentAnimation = contentAnimation { switch contentAnimation.type { - case let .groupInstalled(groupId): - transitionHintInstalledGroupId = groupId + case let .groupInstalled(groupId, scrollToGroup): + if scrollToGroup { + transitionHintInstalledGroupId = groupId + } case let .groupExpanded(groupId): transitionHintExpandedGroupId = groupId case let .groupRemoved(groupId): @@ -5155,7 +5189,7 @@ public final class EmojiPagerContentComponent: Component { guard let strongSelf = self, let component = strongSelf.component else { return } - component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, false) + component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, false, true) }, performItemAction: { [weak self] item, view, rect, layer in guard let strongSelf = self, let component = strongSelf.component else { @@ -5337,7 +5371,7 @@ public final class EmojiPagerContentComponent: Component { guard let strongSelf = self, let component = strongSelf.component else { return } - component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, isPremiumLocked) + component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, isPremiumLocked, true) } )), environment: {}, @@ -5967,6 +6001,10 @@ public final class EmojiPagerContentComponent: Component { self.component = component self.state = state + if component.searchAlwaysActive { + self.isSearchActivated = true + } + component.inputInteractionHolder.inputInteraction?.peekBehavior?.setGestureRecognizerEnabled(view: self, isEnabled: true, itemAtPoint: { [weak self] point in guard let strongSelf = self else { return nil @@ -6019,10 +6057,15 @@ public final class EmojiPagerContentComponent: Component { var transitionHintInstalledGroupId: AnyHashable? var transitionHintExpandedGroupId: AnyHashable? + var keepOffset = false if let contentAnimation = contentAnimation { switch contentAnimation.type { - case let .groupInstalled(groupId): - transitionHintInstalledGroupId = groupId + case let .groupInstalled(groupId, scrollToGroup): + if scrollToGroup { + transitionHintInstalledGroupId = groupId + } else { + keepOffset = true + } case let .groupExpanded(groupId): transitionHintExpandedGroupId = groupId case let .groupRemoved(groupId): @@ -6258,7 +6301,7 @@ public final class EmojiPagerContentComponent: Component { self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: scrollView.isDragging || scrollView.isDecelerating) var animatedScrollOffset: CGFloat = 0.0 - if !anchorItems.isEmpty { + if !anchorItems.isEmpty && !keepOffset { let sortedAnchorItems: [(ItemLayer.Key, CGRect)] = anchorItems.sorted(by: { lhs, rhs in if lhs.value.minY != rhs.value.minY { return lhs.value.minY < rhs.value.minY @@ -6390,20 +6433,24 @@ public final class EmojiPagerContentComponent: Component { strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) } }, deactivated: { [weak self] isFirstResponder in - guard let strongSelf = self else { + guard let strongSelf = self, let component = strongSelf.component else { return } - strongSelf.scrollToTop() - - strongSelf.isSearchActivated = false - strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(false) - if strongSelf.component?.searchInitiallyHidden == false { - if !isFirstResponder { - strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.easeInOut(duration: 0.2)) - } + if let externalCancel = component.inputInteractionHolder.inputInteraction?.externalCancel { + externalCancel() } else { - strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) + strongSelf.scrollToTop() + + strongSelf.isSearchActivated = false + strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(false) + if strongSelf.component?.searchInitiallyHidden == false { + if !isFirstResponder { + strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.easeInOut(duration: 0.2)) + } + } else { + strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) + } } }, updateQuery: { [weak self] query in guard let strongSelf = self else { @@ -7490,6 +7537,7 @@ public final class EmojiPagerContentComponent: Component { displaySearchWithPlaceholder: displaySearchWithPlaceholder, searchCategories: searchCategories, searchInitiallyHidden: searchInitiallyHidden, + searchAlwaysActive: false, searchIsPlaceholderOnly: false, emptySearchResults: nil, enableLongPress: (isReactionSelection && !isQuickReactionSelection) || isStatusSelection, @@ -8010,6 +8058,7 @@ public final class EmojiPagerContentComponent: Component { displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, searchCategories: searchCategories, searchInitiallyHidden: true, + searchAlwaysActive: false, searchIsPlaceholderOnly: searchIsPlaceholderOnly, emptySearchResults: nil, enableLongPress: false, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift new file mode 100644 index 0000000000..59f7560b3d --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift @@ -0,0 +1,466 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import TelegramCore +import Postbox +import AnimationCache +import MultiAnimationRenderer +import AccountContext +import AsyncDisplayKit +import ComponentDisplayAdapters +import PagerComponent +import SwiftSignalKit + +public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode { + private struct Params: Equatable { + var size: CGSize + var leftInset: CGFloat + var rightInset: CGFloat + var bottomInset: CGFloat + var inputHeight: CGFloat + var deviceMetrics: DeviceMetrics + } + + private let context: AccountContext + private var initialFocusId: ItemCollectionId? + private let hasPremiumForUse: Bool + private let hasPremiumForInstallation: Bool + private let parentInputInteraction: EmojiPagerContentComponent.InputInteraction + private var presentationData: PresentationData + + private let keyboardView = ComponentView() + private let panelHostView: PagerExternalTopPanelContainer + private let inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder + + private var params: Params? + + private var itemGroups: [EmojiPagerContentComponent.ItemGroup] = [] + + public var onCancel: (() -> Void)? + + private let emojiSearchDisposable = MetaDisposable() + private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) + private var emojiSearchResultValue: (groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)? + + private var dataDisposable: Disposable? + + public init( + context: AccountContext, + items: [FeaturedStickerPackItem], + initialFocusId: ItemCollectionId?, + hasPremiumForUse: Bool, + hasPremiumForInstallation: Bool, + parentInputInteraction: EmojiPagerContentComponent.InputInteraction + ) { + self.context = context + self.initialFocusId = initialFocusId + self.hasPremiumForUse = hasPremiumForUse + self.hasPremiumForInstallation = hasPremiumForInstallation + self.parentInputInteraction = parentInputInteraction + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.panelHostView = PagerExternalTopPanelContainer() + self.inputInteractionHolder = EmojiPagerContentComponent.InputInteractionHolder() + + super.init() + + for groupItem in items { + var groupItems: [EmojiPagerContentComponent.Item] = [] + for item in groupItem.topItems { + var tintMode: EmojiPagerContentComponent.Item.TintMode = .none + if item.file.isCustomTemplateEmoji { + tintMode = .primary + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + groupItems.append(resultItem) + } + + //TODO:localize + self.itemGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: AnyHashable(groupItem.info.id), + groupId: AnyHashable(groupItem.info.id), + title: groupItem.info.title, + subtitle: nil, + actionButtonTitle: "Add \(groupItem.info.title)", + isFeatured: true, + isPremiumLocked: !self.hasPremiumForInstallation, + isEmbedded: false, + hasClear: false, + collapsedLineCount: 3, + displayPremiumBadges: false, + headerItem: nil, + items: groupItems + )) + } + + self.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak self] groupId, item, sourceView, sourceRect, sourceLayer, isPreview in + guard let self else { + return + } + self.parentInputInteraction.performItemAction(groupId, item, sourceView, sourceRect, sourceLayer, isPreview) + if self.hasPremiumForUse { + self.onCancel?() + } + }, + deleteBackwards: { + }, + openStickerSettings: { + }, + openFeatured: { + }, + openSearch: { + }, + addGroupAction: { [weak self] groupId, isPremiumLocked, _ in + guard let self else { + return + } + self.parentInputInteraction.addGroupAction(groupId, isPremiumLocked, false) + + if !isPremiumLocked { + if self.itemGroups.count == 1 { + self.onCancel?() + } else { + self.itemGroups.removeAll(where: { $0.groupId == groupId }) + self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: groupId)))) + } + } + }, + clearGroup: { _ in + }, + pushController: { _ in + }, + presentController: { _ in + }, + presentGlobalOverlayController: { _ in + }, + navigationController: { + return nil + }, + requestUpdate: { _ in + }, + updateSearchQuery: { [weak self] query in + guard let self else { + return + } + + switch query { + case .none: + self.emojiSearchDisposable.set(nil) + self.emojiSearchResult.set(.single(nil)) + case let .text(rawQuery, languageCode): + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) + + if query.isEmpty { + self.emojiSearchDisposable.set(nil) + self.emojiSearchResult.set(.single(nil)) + } else { + let context = self.context + + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) + if !languageCode.lowercased().hasPrefix("en") { + signal = signal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) + } + } + + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + + let resultSignal = signal + |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + context.engine.stickers.availableReactions(), + hasPremium + ) + |> take(1) + |> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in + var result: [(String, TelegramMediaFile?, String)] = [] + + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + for attribute in item.file.attributes { + switch attribute { + case let .CustomEmoji(_, _, alt, _): + if !item.file.isPremiumEmoji || hasPremium { + if !alt.isEmpty, let keyword = allEmoticons[alt] { + result.append((alt, item.file, keyword)) + } else if alt == query { + result.append((alt, item.file, alt)) + } + } + default: + break + } + } + } + + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for item in result { + if let itemFile = item.1 { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + } + + return [EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + items: items + )] + } + } + + self.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + self.emojiSearchResult.set(.single((result, AnyHashable(query)))) + })) + } + case let .category(value): + let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) + |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for itemFile in files { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + + return .single([EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + items: items + )]) + } + + self.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + self.emojiSearchResult.set(.single((result, AnyHashable(value)))) + })) + } + }, + updateScrollingToItemGroup: { + }, + externalCancel: { [weak self] in + guard let self else { + return + } + self.onCancel?() + }, + chatPeerId: nil, + peekBehavior: nil, + customLayout: nil, + externalBackground: nil, + externalExpansionView: nil, + useOpaqueTheme: true, + hideBackground: false + ) + + self.dataDisposable = ( + self.emojiSearchResult.get() + |> deliverOnMainQueue + ).start(next: { [weak self] emojiSearchResult in + guard let self else { + return + } + self.emojiSearchResultValue = emojiSearchResult + self.update(transition: .immediate) + }) + } + + deinit { + self.emojiSearchDisposable.dispose() + self.dataDisposable?.dispose() + } + + private func update(transition: Transition) { + if let params = self.params { + self.update(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, bottomInset: params.bottomInset, inputHeight: params.inputHeight, deviceMetrics: params.deviceMetrics, transition: transition) + } + } + + public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { + self.update(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: Transition(transition)) + } + + private func update(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: Transition) { + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + let params = Params(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics) + self.params = params + + //TODO:localize + var emojiContent = EmojiPagerContentComponent( + id: "emoji", + context: self.context, + avatarPeer: nil, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + inputInteractionHolder: self.inputInteractionHolder, + panelItemGroups: [], + contentItemGroups: self.itemGroups, + itemLayoutType: .compact, + itemContentUniqueId: "main", + warpContentsOnEdges: false, + displaySearchWithPlaceholder: "Search Emoji", + searchCategories: nil, + searchInitiallyHidden: false, + searchAlwaysActive: true, + searchIsPlaceholderOnly: false, + emptySearchResults: nil, + enableLongPress: false, + selectedItems: Set() + ) + + if let emojiSearchResult = self.emojiSearchResultValue { + var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? + if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { + emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( + text: self.presentationData.strings.EmojiSearch_SearchEmojiEmptyResult, + iconFile: nil + ) + } + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) + } + + let _ = self.keyboardView.update( + transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), + component: AnyComponent(EntityKeyboardComponent( + theme: self.presentationData.theme, + strings: self.presentationData.strings, + isContentInFocus: true, + containerInsets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: bottomInset, right: rightInset), + topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), + emojiContent: emojiContent, + stickerContent: nil, + maskContent: nil, + gifContent: nil, + hasRecentGifs: false, + availableGifSearchEmojies: [], + defaultToEmojiTab: true, + externalTopPanelContainer: self.panelHostView, + externalBottomPanelContainer: nil, + displayTopPanelBackground: true, + topPanelExtensionUpdated: { _, _ in }, + hideInputUpdated: { _, _, _ in }, + hideTopPanelUpdated: { _, _ in + }, + switchToTextInput: {}, + switchToGifSubject: { _ in }, + reorderItems: { _, _ in }, + makeSearchContainerNode: { _ in return nil }, + contentIdUpdated: { _ in }, + deviceMetrics: deviceMetrics, + hiddenInputHeight: 0.0, + inputHeight: 0.0, + displayBottomPanel: false, + isExpanded: false, + clipContentToTopPanel: false, + hidePanels: true + )), + environment: {}, + containerSize: size + ) + if let keyboardComponentView = self.keyboardView.view as? EntityKeyboardComponent.View { + if keyboardComponentView.superview == nil { + self.view.addSubview(keyboardComponentView) + } + transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(), size: size)) + + if let initialFocusId = self.initialFocusId { + self.initialFocusId = nil + + keyboardComponentView.scrollToItemGroup(contentId: "emoji", groupId: AnyHashable(initialFocusId), subgroupId: nil, animated: false) + } + } + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index a9602eb0ff..3d3ff96944 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -286,9 +286,28 @@ final class EmojiSearchSearchBarComponent: Component { if let _ = self.selectedItem, let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) { component.searchTermUpdated(group.identifiers.joined(separator: "")) + + if let itemComponentView = itemView.view.view { + var offset = self.scrollView.contentOffset.x + let maxDistance: CGFloat = 44.0 + if itemComponentView.frame.maxX - offset > self.scrollView.bounds.width - maxDistance { + offset = itemComponentView.frame.maxX - (self.scrollView.bounds.width - maxDistance) + } + if itemComponentView.frame.minX - offset < maxDistance { + offset = itemComponentView.frame.minX - maxDistance + } + offset = max(0.0, min(offset, self.scrollView.contentSize.width - self.scrollView.bounds.width)) + if offset != self.scrollView.contentOffset.x { + self.scrollView.setContentOffset(CGPoint(x: offset, y: 0.0), animated: true) + } + } } else { + let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(), size: self.scrollView.bounds.size)) + self.updateScrolling(transition: transition, fromScrolling: false) + //self.scrollView.setContentOffset(CGPoint(), animated: true) + component.searchTermUpdated(nil) - self.scrollView.setContentOffset(CGPoint(), animated: true) } break @@ -301,8 +320,13 @@ final class EmojiSearchSearchBarComponent: Component { func clearSelection(dispatchEvent: Bool) { if self.selectedItem != nil { self.selectedItem = nil - self.state?.updated(transition: .immediate) - self.scrollView.setContentOffset(CGPoint(), animated: true) + + let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(), size: self.scrollView.bounds.size)) + self.updateScrolling(transition: transition, fromScrolling: false) + + self.state?.updated(transition: transition) + if dispatchEvent { self.component?.searchTermUpdated(nil) } @@ -419,8 +443,18 @@ final class EmojiSearchSearchBarComponent: Component { for (id, itemView) in self.visibleItemViews { if !validItemIds.contains(id) { removedItemIds.append(id) - itemView.view.view?.removeFromSuperview() - itemView.tintView.removeFromSuperview() + + if let itemComponentView = itemView.view.view { + transition.attachAnimation(view: itemComponentView, id: "remove", completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + let tintView = itemView.tintView + transition.attachAnimation(view: tintView, id: "remove", completion: { [weak tintView] _ in + tintView?.removeFromSuperview() + }) + //itemView.view.view?.removeFromSuperview() + //itemView.tintView.removeFromSuperview() } } for id in removedItemIds { @@ -523,10 +557,6 @@ final class EmojiSearchSearchBarComponent: Component { self.isUserInteractionEnabled = false self.textView.view?.isHidden = hasText self.tintTextView.view?.isHidden = hasText - - /*if self.scrollView.contentOffset.x != 0.0 { - self.scrollView.setContentOffset(CGPoint(), animated: true) - }*/ case .inactive: self.isUserInteractionEnabled = true self.textView.view?.isHidden = false diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 6784518cc8..01eef81f38 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -115,6 +115,7 @@ public final class EntityKeyboardComponent: Component { public let displayBottomPanel: Bool public let isExpanded: Bool public let clipContentToTopPanel: Bool + public let hidePanels: Bool public init( theme: PresentationTheme, @@ -145,7 +146,8 @@ public final class EntityKeyboardComponent: Component { inputHeight: CGFloat, displayBottomPanel: Bool, isExpanded: Bool, - clipContentToTopPanel: Bool + clipContentToTopPanel: Bool, + hidePanels: Bool = false ) { self.theme = theme self.strings = strings @@ -176,6 +178,7 @@ public final class EntityKeyboardComponent: Component { self.displayBottomPanel = displayBottomPanel self.isExpanded = isExpanded self.clipContentToTopPanel = clipContentToTopPanel + self.hidePanels = hidePanels } public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { @@ -701,6 +704,8 @@ public final class EntityKeyboardComponent: Component { let panelHideBehavior: PagerComponentPanelHideBehavior if self.searchComponent != nil { panelHideBehavior = .hide + } else if component.hidePanels { + panelHideBehavior = .disable } else if component.isExpanded { panelHideBehavior = .show } else { @@ -914,6 +919,25 @@ public final class EntityKeyboardComponent: Component { } } + public func openCustomSearch(content: EntitySearchContainerNode) { + guard let component = self.component else { + return + } + if self.searchComponent != nil { + return + } + + self.searchComponent = EntitySearchContentComponent( + makeContainerNode: { + return content + }, + dismissSearch: { [weak self] in + self?.closeSearch() + } + ) + component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring))) + } + private func closeSearch() { guard let component = self.component else { return @@ -926,7 +950,7 @@ public final class EntityKeyboardComponent: Component { component.hideInputUpdated(false, false, Transition(animation: .curve(duration: 0.4, curve: .spring))) } - private func scrollToItemGroup(contentId: String, groupId: AnyHashable, subgroupId: Int32?) { + public func scrollToItemGroup(contentId: String, groupId: AnyHashable, subgroupId: Int32?, animated: Bool = true) { guard let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View else { return } @@ -938,7 +962,7 @@ public final class EntityKeyboardComponent: Component { } self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.updateScrollingToItemGroup() - pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId) + pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId, animated: animated) pagerView.collapseTopPanel() } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 7ba5364458..e1c3326610 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -289,7 +289,7 @@ public final class GifPagerContentComponent: Component { self.itemSize = floor((itemHorizontalSpace - self.horizontalSpacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)) let numRowsInGroup = (itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow - self.contentSize = CGSize(width: width, height: self.containerInsets.top + self.containerInsets.bottom + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) + self.contentSize = CGSize(width: width, height: self.searchInsets.top + self.searchHeight + self.containerInsets.top + self.containerInsets.bottom + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) } func frame(at index: Int) -> CGRect { @@ -730,13 +730,18 @@ public final class GifPagerContentComponent: Component { } private func updateScrollingOffset(transition: Transition) { - let isInteracting = scrollView.isDragging || scrollView.isDecelerating + let isInteracting = self.scrollView.isDragging || self.scrollView.isDecelerating if let previousScrollingOffsetValue = self.previousScrollingOffset { - let currentBounds = scrollView.bounds - let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) - let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + let currentBounds = self.scrollView.bounds + var offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + var offsetToBottomEdge = max(0.0, self.scrollView.contentSize.height - currentBounds.maxY) - let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue + if self.scrollView.contentSize.height < self.scrollView.bounds.height * 2.0 { + offsetToTopEdge = 0.0 + offsetToBottomEdge = self.scrollView.contentSize.height + } + + let relativeOffset = self.scrollView.contentOffset.y - previousScrollingOffsetValue self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( relativeOffset: relativeOffset, absoluteOffsetToTopEdge: offsetToTopEdge, @@ -745,19 +750,21 @@ public final class GifPagerContentComponent: Component { isInteracting: isInteracting, transition: transition )) - self.previousScrollingOffset = scrollView.contentOffset.y + self.previousScrollingOffset = self.scrollView.contentOffset.y } - self.previousScrollingOffset = scrollView.contentOffset.y + self.previousScrollingOffset = self.scrollView.contentOffset.y } private func snappedContentOffset(proposedOffset: CGFloat) -> CGFloat { - guard let pagerEnvironment = self.pagerEnvironment else { + guard let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else { return proposedOffset } var proposedOffset = proposedOffset let bounds = self.bounds - if proposedOffset + bounds.height > self.scrollView.contentSize.height - pagerEnvironment.containerInsets.bottom { + if proposedOffset <= itemLayout.searchInsets.top + itemLayout.searchHeight * 0.5 { + proposedOffset = 0.0 + } else if proposedOffset + bounds.height > self.scrollView.contentSize.height - pagerEnvironment.containerInsets.bottom { proposedOffset = self.scrollView.contentSize.height - bounds.height } if proposedOffset < pagerEnvironment.containerInsets.top { @@ -775,6 +782,7 @@ public final class GifPagerContentComponent: Component { transition.setBounds(view: self.scrollView, bounds: currentBounds) self.updateScrollingOffset(transition: transition) + self.updateVisibleItems(attemptSynchronousLoads: false, transition: transition, fromScrolling: true) } private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition, fromScrolling: Bool) { diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index d01c58fd8a..e5a54028ea 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -928,7 +928,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { }, openSearch: { }, - addGroupAction: { groupId, isPremiumLocked in + addGroupAction: { groupId, isPremiumLocked, _ in guard let collectionId = groupId.base as? ItemCollectionId else { return } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 663922c2e5..a5a12656f0 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8662,27 +8662,33 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let subjectFlags: TelegramChatBannedRightsFlags + let subjectFlags: [TelegramChatBannedRightsFlags] switch subject { - case .stickers: - subjectFlags = .banSendStickers - case .mediaRecording, .premiumVoiceMessages: - subjectFlags = .banSendMedia + case .stickers: + subjectFlags = [.banSendStickers] + case .mediaRecording, .premiumVoiceMessages: + subjectFlags = [.banSendVoice, .banSendInstantVideos] } - let bannedPermission: (Int32, Bool)? + var bannedPermission: (Int32, Bool)? = nil if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel { - bannedPermission = channel.hasBannedPermission(subjectFlags) - } else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup { - if group.hasBannedPermission(subjectFlags) { - bannedPermission = (Int32.max, false) - } else { - bannedPermission = nil + for subjectFlag in subjectFlags { + if let value = channel.hasBannedPermission(subjectFlag) { + bannedPermission = value + break + } + } + } else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup { + for subjectFlag in subjectFlags { + if group.hasBannedPermission(subjectFlag) { + bannedPermission = (Int32.max, false) + break + } } - } else { - bannedPermission = nil } + var displayToast = false + if let (untilDate, personal) = bannedPermission { let banDescription: String switch subject { @@ -8700,7 +8706,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if personal { banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia } else { - banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedMedia + banDescription = strongSelf.restrictedSendingContentsText() + displayToast = true } case .premiumVoiceMessages: banDescription = "" @@ -8714,37 +8721,41 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch displayType { case .tooltip: - var rect: CGRect? - let isStickers: Bool = subject == .stickers - switch subject { - case .stickers: - rect = strongSelf.chatDisplayNode.frameForStickersButton() - if var rectValue = rect, let actionRect = strongSelf.chatDisplayNode.frameForInputActionButton() { - rectValue.origin.y = actionRect.minY - rect = rectValue - } - case .mediaRecording, .premiumVoiceMessages: - rect = strongSelf.chatDisplayNode.frameForInputActionButton() - } - - if let tooltipController = strongSelf.mediaRestrictedTooltipController, strongSelf.mediaRestrictedTooltipControllerMode == isStickers { - tooltipController.updateContent(.text(banDescription), animated: true, extendTimer: true) - } else if let rect = rect { - strongSelf.mediaRestrictedTooltipController?.dismiss() - let tooltipController = TooltipController(content: .text(banDescription), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize) - strongSelf.mediaRestrictedTooltipController = tooltipController - strongSelf.mediaRestrictedTooltipControllerMode = isStickers - tooltipController.dismissed = { [weak tooltipController] _ in - if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRestrictedTooltipController === tooltipController { - strongSelf.mediaRestrictedTooltipController = nil + if displayToast { + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: banDescription)) + } else { + var rect: CGRect? + let isStickers: Bool = subject == .stickers + switch subject { + case .stickers: + rect = strongSelf.chatDisplayNode.frameForStickersButton() + if var rectValue = rect, let actionRect = strongSelf.chatDisplayNode.frameForInputActionButton() { + rectValue.origin.y = actionRect.minY + rect = rectValue } + case .mediaRecording, .premiumVoiceMessages: + rect = strongSelf.chatDisplayNode.frameForInputActionButton() } - strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { - if let strongSelf = self { - return (strongSelf.chatDisplayNode, rect) + + if let tooltipController = strongSelf.mediaRestrictedTooltipController, strongSelf.mediaRestrictedTooltipControllerMode == isStickers { + tooltipController.updateContent(.text(banDescription), animated: true, extendTimer: true) + } else if let rect = rect { + strongSelf.mediaRestrictedTooltipController?.dismiss() + let tooltipController = TooltipController(content: .text(banDescription), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize) + strongSelf.mediaRestrictedTooltipController = tooltipController + strongSelf.mediaRestrictedTooltipControllerMode = isStickers + tooltipController.dismissed = { [weak tooltipController] _ in + if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRestrictedTooltipController === tooltipController { + strongSelf.mediaRestrictedTooltipController = nil + } } - return nil - })) + strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { + if let strongSelf = self { + return (strongSelf.chatDisplayNode, rect) + } + return nil + })) + } } case .alert: strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: banDescription, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 6139637e7c..6aa729b850 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -930,7 +930,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return } //TODO:localize - controller.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send text messages.")) + controller.controllerInteraction?.displayUndo(.info(title: nil, text: controller.restrictedSendingContentsText())) } else { strongSelf.ensureFocused() } diff --git a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift index 5793ab3096..f5b97b01b2 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift @@ -14,6 +14,12 @@ import AnimationCache import MultiAnimationRenderer import TextFormat import ChatControllerInteraction +import ContextUI +import SwiftSignalKit +import PremiumUI +import StickerPeekUI +import UndoUI +import Pasteboard private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable { case symbol(String) @@ -119,6 +125,8 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer + private weak var peekController: PeekController? + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) { self.animationCache = chatPresentationContext.animationCache self.animationRenderer = chatPresentationContext.animationRenderer @@ -161,6 +169,281 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.listView) + + let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in + guard let self else { + return nil + } + return self.peekContentAtPoint(point: point) + }, present: { [weak self] content, sourceView, sourceRect in + guard let strongSelf = self else { + return nil + } + + let presentationData = strongSelf.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) + self?.requestDisableStickerAnimations?(visible) + self?.simulateUpdateLayout(isVisible: !visible) + }*/ + strongSelf.peekController = controller + strongSelf.interfaceInteraction?.presentController(controller, nil) + return controller + }, updateContent: { [weak self] content in + guard let strongSelf = self else { + return + } + + let _ = strongSelf + }) + self.view.addGestureRecognizer(peekRecognizer) + } + + private func peekContentAtPoint(point: CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? { + guard let presentationInterfaceState = self.presentationInterfaceState else { + return nil + } + guard let chatPeerId = presentationInterfaceState.renderedPeer?.peer?.id else { + return nil + } + + var maybeFile: TelegramMediaFile? + var maybeItemLayer: CALayer? + + self.listView.forEachItemNode { itemNode in + if let itemNode = itemNode as? EmojisChatInputPanelItemNode, let item = itemNode.item { + let localPoint = self.view.convert(point, to: itemNode.view) + if itemNode.view.bounds.contains(localPoint) { + maybeFile = item.file + maybeItemLayer = itemNode.layer + } + } + } + + guard let file = maybeFile else { + return nil + } + guard let itemLayer = maybeItemLayer else { + return nil + } + + let _ = chatPeerId + let _ = file + let _ = itemLayer + + var collectionId: ItemCollectionId? + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id) + default: + break + } + } + } + + var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] + if let collectionId { + bubbleUpEmojiOrStickersets.append(collectionId) + } + + let context = self.context + let accountPeerId = context.account.peerId + + let _ = bubbleUpEmojiOrStickersets + let _ = context + let _ = accountPeerId + + return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)) + |> map { peer -> Bool in + var hasPremium = false + if case let .user(user) = peer, user.isPremium { + hasPremium = true + } + return hasPremium + } + |> deliverOnMainQueue + |> map { [weak self, weak itemLayer] hasPremium -> (UIView, CGRect, PeekControllerContent)? in + guard let strongSelf = self, let itemLayer = itemLayer else { + return nil + } + + let _ = strongSelf + let _ = itemLayer + + var menuItems: [ContextMenuItem] = [] + menuItems.removeAll() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let _ = presentationData + + var isLocked = false + if !hasPremium { + isLocked = file.isPremiumEmoji + if isLocked && chatPeerId == context.account.peerId { + isLocked = false + } + } + + if let interaction = strongSelf.interfaceInteraction { + let _ = interaction + + let sendEmoji: (TelegramMediaFile) -> Void = { file in + guard let self else { + return + } + guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else { + return + } + + var text = "." + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, _, displayText, stickerPackReference): + text = displayText + + var packId: ItemCollectionId? + if case let .id(id, _) = stickerPackReference { + packId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id) + } + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: packId, fileId: file.fileId.id, file: file) + break loop + default: + break + } + } + + if let emojiAttribute { + controller.controllerInteraction?.sendEmoji(text, emojiAttribute, true) + } + } + let setStatus: (TelegramMediaFile) -> Void = { file in + guard let self else { + return + } + guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else { + return + } + + let _ = self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).start() + + var animateInAsReplacement = false + animateInAsReplacement = false + /*if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { + currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() + strongSelf.currentUndoOverlayController = nil + animateInAsReplacement = true + }*/ + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + //TODO:localize + let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, title: nil, text: "Your emoji status has been updated.", undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) + //strongSelf.currentUndoOverlayController = controller + controller.controllerInteraction?.presentController(undoController, nil) + } + let copyEmoji: (TelegramMediaFile) -> Void = { file in + var text = "." + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, _, displayText, _): + text = displayText + + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) + break loop + default: + break + } + } + + if let _ = emojiAttribute { + storeMessageTextInPasteboard(text, entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))]) + } + } + + //TODO:localize + menuItems.append(.action(ContextMenuActionItem(text: "Send Emoji", icon: { theme in + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) { + return generateImage(image.size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + if let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) + } + }) + } else { + return nil + } + }, action: { _, f in + sendEmoji(file) + f(.default) + }))) + + //TODO:localize + menuItems.append(.action(ContextMenuActionItem(text: "Set as Status", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Smile"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + guard let strongSelf = self else { + return + } + + if hasPremium { + setStatus(file) + } else { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: { + let controller = PremiumIntroScreen(context: context, source: .animatedEmoji) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(controller) + } + }))) + + //TODO:localize + menuItems.append(.action(ContextMenuActionItem(text: "Copy Emoji", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + copyEmoji(file) + f(.default) + }))) + } + + if menuItems.isEmpty { + return nil + } + + let content = StickerPreviewPeekContent(account: context.account, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked, menu: menuItems, openPremiumIntro: { [weak self] in + guard let self else { + return + } + guard let interfaceInteraction = self.interfaceInteraction else { + return + } + + let _ = self + let _ = interfaceInteraction + + let controller = PremiumIntroScreen(context: context, source: .stickers) + //let _ = controller + + interfaceInteraction.getNavigationController()?.pushViewController(controller) + }) + let _ = content + //return nil + + return (strongSelf.view, itemLayer.convert(itemLayer.bounds, to: strongSelf.view.layer), content) + } } func updateResults(_ results: [(String, TelegramMediaFile?, String)]) { diff --git a/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift b/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift index 7bd6a012c9..d193f2c4dc 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift @@ -17,7 +17,7 @@ final class EmojisChatInputPanelItem: ListViewItem { fileprivate let theme: PresentationTheme fileprivate let symbol: String fileprivate let text: String - fileprivate let file: TelegramMediaFile? + let file: TelegramMediaFile? fileprivate let animationCache: AnimationCache fileprivate let animationRenderer: MultiAnimationRenderer private let emojiSelected: (String, TelegramMediaFile?) -> Void @@ -94,6 +94,8 @@ final class EmojisChatInputPanelItemNode: ListViewItemNode { private let symbolNode: TextNode private var emojiView: EmojiTextAttachmentView? + var item: EmojisChatInputPanelItem? + init() { self.symbolNode = TextNode() self.symbolNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) @@ -123,6 +125,8 @@ final class EmojisChatInputPanelItemNode: ListViewItemNode { return (nodeLayout, { _ in if let strongSelf = self { + strongSelf.item = item + let _ = symbolApply() strongSelf.symbolNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((EmojisChatInputPanelItemNode.itemSize.width - symbolLayout.size.width) / 2.0), y: 0.0), size: symbolLayout.size)