mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-03 21:16:35 +00:00
New GIF search design
This commit is contained in:
parent
a95da7736f
commit
4bc196d6db
@ -99,6 +99,8 @@
|
||||
09C3466D2167D63A00B76780 /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C3466C2167D63A00B76780 /* Accessibility.swift */; };
|
||||
09C500242142BA6400EF253E /* ItemListWebsiteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */; };
|
||||
09C9EA3821A044B500E90146 /* StringForDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C9EA3721A044B500E90146 /* StringForDuration.swift */; };
|
||||
09CE95002232729A00A7D2C3 /* StickerPaneSearchContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CE94FF2232729A00A7D2C3 /* StickerPaneSearchContentNode.swift */; };
|
||||
09CE9502223272B700A7D2C3 /* GifPaneSearchContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CE9501223272B700A7D2C3 /* GifPaneSearchContentNode.swift */; };
|
||||
09D304152173C0E900C00567 /* WatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304142173C0E900C00567 /* WatchManager.swift */; };
|
||||
09D304182173C15700C00567 /* WatchSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304172173C15700C00567 /* WatchSettingsController.swift */; };
|
||||
09D96899221DE92600B1458A /* ID3ArtworkReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D96898221DE92600B1458A /* ID3ArtworkReader.swift */; };
|
||||
@ -225,7 +227,7 @@
|
||||
D02B198A21F1DA9E0094A764 /* SharedAccountContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02B198921F1DA9E0094A764 /* SharedAccountContext.swift */; };
|
||||
D02B198E21FA453F0094A764 /* AccountUserInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02B198D21FA453F0094A764 /* AccountUserInterface.swift */; };
|
||||
D02B2B9820810DA00062476B /* StickerPaneSearchStickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02B2B9720810DA00062476B /* StickerPaneSearchStickerItem.swift */; };
|
||||
D02B676320800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02B676220800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift */; };
|
||||
D02B676320800A00001A864A /* PaneSearchBarPlaceholderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02B676220800A00001A864A /* PaneSearchBarPlaceholderItem.swift */; };
|
||||
D02C81712177729000CD1006 /* NotificationExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C81702177729000CD1006 /* NotificationExceptions.swift */; };
|
||||
D02C81732177AC5900CD1006 /* NotificationSearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */; };
|
||||
D02D60AE206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D60AD206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift */; };
|
||||
@ -435,8 +437,8 @@
|
||||
D0AD02EA1FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AD02E91FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift */; };
|
||||
D0AD02EC20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AD02EB20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift */; };
|
||||
D0ADF966212E05A300310BBC /* TonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADF965212E05A300310BBC /* TonePlayer.swift */; };
|
||||
D0AEAE252080D6830013176E /* StickerPaneSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AEAE242080D6830013176E /* StickerPaneSearchContainerNode.swift */; };
|
||||
D0AEAE272080D6970013176E /* StickerPaneSearchBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AEAE262080D6970013176E /* StickerPaneSearchBarNode.swift */; };
|
||||
D0AEAE252080D6830013176E /* PaneSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AEAE242080D6830013176E /* PaneSearchContainerNode.swift */; };
|
||||
D0AEAE272080D6970013176E /* PaneSearchBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AEAE262080D6970013176E /* PaneSearchBarNode.swift */; };
|
||||
D0AEAE292080FD660013176E /* StickerPaneSearchGlobaltem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AEAE282080FD660013176E /* StickerPaneSearchGlobaltem.swift */; };
|
||||
D0AF323A1FB1D8D60097362B /* ChatOverlayNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF32391FB1D8D60097362B /* ChatOverlayNavigationBar.swift */; };
|
||||
D0AFCC791F4C8D2C000720C6 /* InstantPageSlideshowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AFCC781F4C8D2C000720C6 /* InstantPageSlideshowItem.swift */; };
|
||||
@ -1246,6 +1248,8 @@
|
||||
09C3466C2167D63A00B76780 /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = "<group>"; };
|
||||
09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListWebsiteItem.swift; sourceTree = "<group>"; };
|
||||
09C9EA3721A044B500E90146 /* StringForDuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringForDuration.swift; sourceTree = "<group>"; };
|
||||
09CE94FF2232729A00A7D2C3 /* StickerPaneSearchContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchContentNode.swift; sourceTree = "<group>"; };
|
||||
09CE9501223272B700A7D2C3 /* GifPaneSearchContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifPaneSearchContentNode.swift; sourceTree = "<group>"; };
|
||||
09D304142173C0E900C00567 /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = "<group>"; };
|
||||
09D304172173C15700C00567 /* WatchSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsController.swift; sourceTree = "<group>"; };
|
||||
09D96898221DE92600B1458A /* ID3ArtworkReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ID3ArtworkReader.swift; sourceTree = "<group>"; };
|
||||
@ -1468,7 +1472,7 @@
|
||||
D02B198921F1DA9E0094A764 /* SharedAccountContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedAccountContext.swift; sourceTree = "<group>"; };
|
||||
D02B198D21FA453F0094A764 /* AccountUserInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountUserInterface.swift; sourceTree = "<group>"; };
|
||||
D02B2B9720810DA00062476B /* StickerPaneSearchStickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchStickerItem.swift; sourceTree = "<group>"; };
|
||||
D02B676220800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchBarPlaceholderItem.swift; sourceTree = "<group>"; };
|
||||
D02B676220800A00001A864A /* PaneSearchBarPlaceholderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaneSearchBarPlaceholderItem.swift; sourceTree = "<group>"; };
|
||||
D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = "<group>"; };
|
||||
D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = "<group>"; };
|
||||
D02C81702177729000CD1006 /* NotificationExceptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationExceptions.swift; sourceTree = "<group>"; };
|
||||
@ -1881,8 +1885,8 @@
|
||||
D0AD02E91FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageLiveLocationTextNode.swift; sourceTree = "<group>"; };
|
||||
D0AD02EB20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageLiveLocationPositionNode.swift; sourceTree = "<group>"; };
|
||||
D0ADF965212E05A300310BBC /* TonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonePlayer.swift; sourceTree = "<group>"; };
|
||||
D0AEAE242080D6830013176E /* StickerPaneSearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchContainerNode.swift; sourceTree = "<group>"; };
|
||||
D0AEAE262080D6970013176E /* StickerPaneSearchBarNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchBarNode.swift; sourceTree = "<group>"; };
|
||||
D0AEAE242080D6830013176E /* PaneSearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaneSearchContainerNode.swift; sourceTree = "<group>"; };
|
||||
D0AEAE262080D6970013176E /* PaneSearchBarNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaneSearchBarNode.swift; sourceTree = "<group>"; };
|
||||
D0AEAE282080FD660013176E /* StickerPaneSearchGlobaltem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchGlobaltem.swift; sourceTree = "<group>"; };
|
||||
D0AF32391FB1D8D60097362B /* ChatOverlayNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatOverlayNavigationBar.swift; sourceTree = "<group>"; };
|
||||
D0AFCC781F4C8D2C000720C6 /* InstantPageSlideshowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSlideshowItem.swift; sourceTree = "<group>"; };
|
||||
@ -2769,9 +2773,11 @@
|
||||
D002A0DA1E9C190700A81812 /* SoftwareVideoThumbnailLayer.swift */,
|
||||
D0575AEC1E9FF1AD006F2541 /* ChatMediaInputTrendingPane.swift */,
|
||||
D04203142037162700490EA5 /* MediaInputPaneTrendingItem.swift */,
|
||||
D02B676220800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift */,
|
||||
D0AEAE262080D6970013176E /* StickerPaneSearchBarNode.swift */,
|
||||
D0AEAE242080D6830013176E /* StickerPaneSearchContainerNode.swift */,
|
||||
D02B676220800A00001A864A /* PaneSearchBarPlaceholderItem.swift */,
|
||||
D0AEAE262080D6970013176E /* PaneSearchBarNode.swift */,
|
||||
D0AEAE242080D6830013176E /* PaneSearchContainerNode.swift */,
|
||||
09CE9501223272B700A7D2C3 /* GifPaneSearchContentNode.swift */,
|
||||
09CE94FF2232729A00A7D2C3 /* StickerPaneSearchContentNode.swift */,
|
||||
D0AEAE282080FD660013176E /* StickerPaneSearchGlobaltem.swift */,
|
||||
D02B2B9720810DA00062476B /* StickerPaneSearchStickerItem.swift */,
|
||||
D069F5CF212700B90000565A /* StickerPanePeerSpecificSetupGridItem.swift */,
|
||||
@ -5385,6 +5391,7 @@
|
||||
09DD88ED21BDC8B7000766BC /* FormEditableBlockItemNode.swift in Sources */,
|
||||
D079FCE11F05C9380038FADE /* BotReceiptControllerNode.swift in Sources */,
|
||||
D0FA08CA2049BEAC00DD23FC /* ChatEmptyNode.swift in Sources */,
|
||||
09CE95002232729A00A7D2C3 /* StickerPaneSearchContentNode.swift in Sources */,
|
||||
D053DADC201AAAB100993D32 /* ChatTextInputMenu.swift in Sources */,
|
||||
0962E66321B3513100245FD9 /* WebSearchControllerNode.swift in Sources */,
|
||||
D0EC6D1A1EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */,
|
||||
@ -5503,7 +5510,7 @@
|
||||
D0B2F76A2052920D00D3BFB9 /* UserInfoEditingPhoneItem.swift in Sources */,
|
||||
09749BC921F1BBA1008FDDE9 /* CallFeedbackController.swift in Sources */,
|
||||
099529FA21DD8A3100805E13 /* NavigationBarSearchContentNode.swift in Sources */,
|
||||
D0AEAE272080D6970013176E /* StickerPaneSearchBarNode.swift in Sources */,
|
||||
D0AEAE272080D6970013176E /* PaneSearchBarNode.swift in Sources */,
|
||||
D0EC6D4F1EB9F58800EBF1C3 /* ChatListSearchItem.swift in Sources */,
|
||||
D0EC6D501EB9F58800EBF1C3 /* ChatListNodeEntries.swift in Sources */,
|
||||
D0EC6D511EB9F58800EBF1C3 /* ChatListViewTransition.swift in Sources */,
|
||||
@ -5635,7 +5642,7 @@
|
||||
0902838821931D960067EFBD /* LanguageSuggestionController.swift in Sources */,
|
||||
D0E8B8A72044339500605593 /* PresentationCallToneData.swift in Sources */,
|
||||
D0F19F6420E5A15B00EEC860 /* ChatMediaInputPeerSpecificItem.swift in Sources */,
|
||||
D0AEAE252080D6830013176E /* StickerPaneSearchContainerNode.swift in Sources */,
|
||||
D0AEAE252080D6830013176E /* PaneSearchContainerNode.swift in Sources */,
|
||||
D01DBA9B209CC6AD00C64E64 /* ChatLinkPreview.swift in Sources */,
|
||||
D044A0FB20BDC40C00326FAC /* CachedChannelAdmins.swift in Sources */,
|
||||
D0EC6D901EB9F58900EBF1C3 /* ChatMessageBubbleContentNode.swift in Sources */,
|
||||
@ -5693,6 +5700,7 @@
|
||||
D093D82020699A7300BC3599 /* FormController.swift in Sources */,
|
||||
D0EC6DA11EB9F58900EBF1C3 /* ChatMessageSelectionNode.swift in Sources */,
|
||||
D0EC6DA21EB9F58900EBF1C3 /* ChatMessageBubbleImages.swift in Sources */,
|
||||
09CE9502223272B700A7D2C3 /* GifPaneSearchContentNode.swift in Sources */,
|
||||
D0EC6DA31EB9F58900EBF1C3 /* ChatMessageDateHeader.swift in Sources */,
|
||||
D0EC6DA41EB9F58900EBF1C3 /* ChatMessageActionButtonsNode.swift in Sources */,
|
||||
D0EC6DA51EB9F58900EBF1C3 /* ChatBotInfoItem.swift in Sources */,
|
||||
@ -6041,7 +6049,7 @@
|
||||
D0EC6E511EB9F58900EBF1C3 /* ChannelBlacklistController.swift in Sources */,
|
||||
D0EC6E521EB9F58900EBF1C3 /* ChannelInfoController.swift in Sources */,
|
||||
D0EC6E531EB9F58900EBF1C3 /* ChannelMembersController.swift in Sources */,
|
||||
D02B676320800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift in Sources */,
|
||||
D02B676320800A00001A864A /* PaneSearchBarPlaceholderItem.swift in Sources */,
|
||||
D093D8242069A06600BC3599 /* FormControllerScrollerNode.swift in Sources */,
|
||||
D081E106217F5834003CD921 /* LanguageLinkPreviewControllerNode.swift in Sources */,
|
||||
D093D7E72063E57F00BC3599 /* BotPaymentActionItemNode.swift in Sources */,
|
||||
|
||||
@ -552,7 +552,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }.updatedInputMode { current in
|
||||
if case let .media(mode, maybeExpanded) = current, let expanded = maybeExpanded, case .content = expanded {
|
||||
if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil {
|
||||
return .media(mode: mode, expanded: nil)
|
||||
}
|
||||
return current
|
||||
|
||||
@ -231,11 +231,11 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
|
||||
|
||||
switch chatPresentationInterfaceState.inputMode {
|
||||
case .media:
|
||||
if contextPlaceholder == nil && chatPresentationInterfaceState.interfaceState.editMessage == nil && chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0, case .media(.gif, _) = chatPresentationInterfaceState.inputMode {
|
||||
let baseFontSize: CGFloat = max(17.0, chatPresentationInterfaceState.fontSize.baseDisplaySize)
|
||||
|
||||
contextPlaceholder = NSAttributedString(string: "@gif", font: Font.regular(baseFontSize), textColor: chatPresentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor)
|
||||
}
|
||||
// if contextPlaceholder == nil && chatPresentationInterfaceState.interfaceState.editMessage == nil && chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0, case .media(.gif, _) = chatPresentationInterfaceState.inputMode {
|
||||
// let baseFontSize: CGFloat = max(17.0, chatPresentationInterfaceState.fontSize.baseDisplaySize)
|
||||
//
|
||||
// contextPlaceholder = NSAttributedString(string: "@gif", font: Font.regular(baseFontSize), textColor: chatPresentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor)
|
||||
// }
|
||||
accessoryItems.append(.keyboard)
|
||||
return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
|
||||
case .inputButtons:
|
||||
|
||||
@ -5,12 +5,49 @@ import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) {
|
||||
let searchBarHeight: CGFloat = 60.0
|
||||
|
||||
if multiplexedNode.contentOffset.y > searchBarHeight * 0.6 {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// var searchItemNode: ListViewItemNode?
|
||||
// var nextItemNode: ListViewItemNode?
|
||||
//
|
||||
// listNode.forEachItemNode({ itemNode in
|
||||
// if let itemNode = itemNode as? ChatListSearchItemNode {
|
||||
// searchItemNode = itemNode
|
||||
// } else if searchItemNode != nil && nextItemNode == nil {
|
||||
// nextItemNode = itemNode as? ListViewItemNode
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// if let searchItemNode = searchItemNode {
|
||||
// let itemFrame = searchItemNode.apparentFrame
|
||||
// if itemFrame.contains(CGPoint(x: 0.0, y: listNode.insets.top)) {
|
||||
// if itemFrame.minY + itemFrame.height * 0.6 < listNode.insets.top {
|
||||
// if let nextItemNode = nextItemNode {
|
||||
// listNode.ensureItemNodeVisibleAtTopInset(nextItemNode)
|
||||
// }
|
||||
// } else {
|
||||
// listNode.ensureItemNodeVisibleAtTopInset(searchItemNode)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
private let account: Account
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
|
||||
private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void
|
||||
private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void
|
||||
|
||||
let searchPlaceholderNode: PaneSearchBarPlaceholderNode
|
||||
private var multiplexedNode: MultiplexedVideoNode?
|
||||
private let emptyNode: ImmediateTextNode
|
||||
|
||||
@ -27,6 +64,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
self.paneDidScroll = paneDidScroll
|
||||
self.fixPaneScroll = fixPaneScroll
|
||||
|
||||
self.searchPlaceholderNode = PaneSearchBarPlaceholderNode()
|
||||
|
||||
self.emptyNode = ImmediateTextNode()
|
||||
self.emptyNode.isUserInteractionEnabled = false
|
||||
self.emptyNode.attributedText = NSAttributedString(string: strings.Conversation_EmptyGifPanelPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
@ -37,6 +76,10 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
|
||||
self.addSubnode(self.emptyNode)
|
||||
|
||||
self.searchPlaceholderNode.activate = { [weak self] in
|
||||
self?.inputNodeInteraction?.toggleSearch(true, .gif)
|
||||
}
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
}
|
||||
|
||||
@ -45,25 +88,43 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.backgroundColor = theme.chat.inputMediaPanel.gifsBackgroundColor
|
||||
self.emptyNode.attributedText = NSAttributedString(string: strings.Conversation_EmptyGifPanelPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
|
||||
self.searchPlaceholderNode.setup(theme: theme, strings: strings, type: .gifs)
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.updateLayout(size: layout.0, topInset: layout.1, bottomInset: layout.2, isExpanded: layout.3, isVisible: layout.4, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, transition: ContainedViewLayoutTransition) {
|
||||
var changedIsExpanded = false
|
||||
if let (_, _, _, previousIsExpanded, _) = self.validLayout {
|
||||
if previousIsExpanded != isExpanded {
|
||||
changedIsExpanded = true
|
||||
}
|
||||
}
|
||||
self.validLayout = (size, topInset, bottomInset, isExpanded, isVisible)
|
||||
|
||||
let emptySize = self.emptyNode.updateLayout(size)
|
||||
transition.updateFrame(node: self.emptyNode, frame: CGRect(origin: CGPoint(x: floor(size.width - emptySize.width) / 2.0, y: topInset + floor(size.height - topInset - emptySize.height) / 2.0), size: emptySize))
|
||||
|
||||
if let multiplexedNode = self.multiplexedNode {
|
||||
multiplexedNode.topInset = topInset
|
||||
let previousBounds = multiplexedNode.layer.bounds
|
||||
multiplexedNode.topInset = topInset + 60.0
|
||||
multiplexedNode.bottomInset = bottomInset
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
transition.updateFrame(layer: multiplexedNode.layer, frame: nodeFrame)
|
||||
|
||||
var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size)
|
||||
if changedIsExpanded {
|
||||
targetBounds.origin.y = isExpanded ? 0.0 : 60.0
|
||||
}
|
||||
|
||||
transition.updateBounds(layer: multiplexedNode.layer, bounds: targetBounds)
|
||||
transition.updatePosition(layer: multiplexedNode.layer, position: nodeFrame.center)
|
||||
|
||||
multiplexedNode.updateLayout(size: nodeFrame.size, transition: transition)
|
||||
self.searchPlaceholderNode.frame = CGRect(x: 0.0, y: 41.0, width: size.width, height: 56.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +147,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
self.view.addSubview(multiplexedNode)
|
||||
let initialOrder = Atomic<[MediaId]?>(value: nil)
|
||||
multiplexedNode.addSubnode(self.searchPlaceholderNode)
|
||||
|
||||
let gifs = self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)])
|
||||
|> map { view -> [FileMediaReference] in
|
||||
var recentGifs: OrderedItemListView?
|
||||
@ -104,8 +166,12 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
}
|
||||
self.disposable.set((gifs |> deliverOnMainQueue).start(next: { [weak self] gifs in
|
||||
if let strongSelf = self {
|
||||
let previousFiles = strongSelf.multiplexedNode?.files
|
||||
strongSelf.multiplexedNode?.files = gifs
|
||||
strongSelf.emptyNode.isHidden = !gifs.isEmpty
|
||||
if (previousFiles ?? []).isEmpty {
|
||||
strongSelf.multiplexedNode?.contentOffset = CGPoint(x: 0.0, y: 60.0)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
@ -117,7 +183,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let absoluteOffset = -offset
|
||||
let absoluteOffset = -offset + 60.0
|
||||
var delta: CGFloat = 0.0
|
||||
if let didScrollPreviousOffset = strongSelf.didScrollPreviousOffset {
|
||||
delta = absoluteOffset - didScrollPreviousOffset
|
||||
@ -135,6 +201,10 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
if let didScrollPreviousState = strongSelf.didScrollPreviousState {
|
||||
strongSelf.fixPaneScroll(strongSelf, didScrollPreviousState)
|
||||
}
|
||||
|
||||
if let multiplexedNode = strongSelf.multiplexedNode {
|
||||
fixListScrolling(strongSelf.multiplexedNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,8 +136,8 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable {
|
||||
func item(account: Account, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem {
|
||||
switch self {
|
||||
case let .search(theme, strings):
|
||||
return StickerPaneSearchBarPlaceholderItem(theme: theme, strings: strings, activate: {
|
||||
inputNodeInteraction.toggleSearch(true)
|
||||
return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: {
|
||||
inputNodeInteraction.toggleSearch(true, .sticker)
|
||||
})
|
||||
case let .peerSpecificSetup(theme, strings, dismissed):
|
||||
return StickerPanePeerSpecificSetupGridItem(theme: theme, strings: strings, setup: {
|
||||
|
||||
@ -322,7 +322,7 @@ private enum StickerPacksCollectionUpdate {
|
||||
final class ChatMediaInputNodeInteraction {
|
||||
let navigateToCollectionId: (ItemCollectionId) -> Void
|
||||
let openSettings: () -> Void
|
||||
let toggleSearch: (Bool) -> Void
|
||||
let toggleSearch: (Bool, ChatMediaInputSearchMode?) -> Void
|
||||
let openPeerSpecificSettings: () -> Void
|
||||
let dismissPeerSpecificSettings: () -> Void
|
||||
|
||||
@ -331,7 +331,7 @@ final class ChatMediaInputNodeInteraction {
|
||||
var previewedStickerPackItem: StickerPreviewPeekItem?
|
||||
var appearanceTransition: CGFloat = 1.0
|
||||
|
||||
init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void) {
|
||||
init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void) {
|
||||
self.navigateToCollectionId = navigateToCollectionId
|
||||
self.openSettings = openSettings
|
||||
self.toggleSearch = toggleSearch
|
||||
@ -398,8 +398,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private let listView: ListView
|
||||
private var stickerSearchContainerNode: StickerPaneSearchContainerNode?
|
||||
private let stickerSearchContainerNodeLoadedDisposable = MetaDisposable()
|
||||
private var searchContainerNode: PaneSearchContainerNode?
|
||||
private let searchContainerNodeLoadedDisposable = MetaDisposable()
|
||||
|
||||
private let stickerPane: ChatMediaInputStickerPane
|
||||
private var animatingStickerPaneOut = false
|
||||
@ -514,32 +514,34 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction.navigationController()?.pushViewController(installedStickerPacksController(context: context, mode: .modal))
|
||||
}
|
||||
}, toggleSearch: { [weak self] value in
|
||||
}, toggleSearch: { [weak self] value, searchMode in
|
||||
if let strongSelf = self {
|
||||
if value {
|
||||
let stickerSearchContainerNode: StickerPaneSearchContainerNode
|
||||
if let current = strongSelf.stickerSearchContainerNode {
|
||||
stickerSearchContainerNode = current
|
||||
if let searchMode = searchMode, value {
|
||||
var searchContainerNode: PaneSearchContainerNode?
|
||||
if let current = strongSelf.searchContainerNode {
|
||||
searchContainerNode = current
|
||||
} else {
|
||||
stickerSearchContainerNode = StickerPaneSearchContainerNode(context: strongSelf.context, theme: strongSelf.theme, strings: strongSelf.strings, controllerInteraction: strongSelf.controllerInteraction, inputNodeInteraction: strongSelf.inputNodeInteraction, cancel: {
|
||||
self?.stickerSearchContainerNode?.deactivate()
|
||||
self?.inputNodeInteraction.toggleSearch(false)
|
||||
searchContainerNode = PaneSearchContainerNode(context: strongSelf.context, theme: strongSelf.theme, strings: strongSelf.strings, controllerInteraction: strongSelf.controllerInteraction, inputNodeInteraction: strongSelf.inputNodeInteraction, mode: searchMode, cancel: {
|
||||
self?.searchContainerNode?.deactivate()
|
||||
self?.inputNodeInteraction.toggleSearch(false, nil)
|
||||
})
|
||||
strongSelf.stickerSearchContainerNode = stickerSearchContainerNode
|
||||
strongSelf.searchContainerNode = searchContainerNode
|
||||
}
|
||||
strongSelf.stickerSearchContainerNodeLoadedDisposable.set((stickerSearchContainerNode.ready
|
||||
|> deliverOnMainQueue).start(next: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction.updateInputMode { current in
|
||||
switch current {
|
||||
case let .media(mode, _):
|
||||
return .media(mode: mode, expanded: .search)
|
||||
default:
|
||||
return current
|
||||
if let searchContainerNode = searchContainerNode {
|
||||
strongSelf.searchContainerNodeLoadedDisposable.set((searchContainerNode.ready
|
||||
|> deliverOnMainQueue).start(next: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction.updateInputMode { current in
|
||||
switch current {
|
||||
case let .media(mode, _):
|
||||
return .media(mode: mode, expanded: .search(searchMode))
|
||||
default:
|
||||
return current
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
strongSelf.controllerInteraction.updateInputMode { current in
|
||||
switch current {
|
||||
@ -762,6 +764,9 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.currentStickerPacksCollectionPosition = .initial
|
||||
self.itemCollectionsViewPosition.set(.single(.initial))
|
||||
|
||||
self.stickerPane.inputNodeInteraction = self.inputNodeInteraction
|
||||
self.gifPane.inputNodeInteraction = self.inputNodeInteraction
|
||||
|
||||
paneDidScrollImpl = { [weak self] pane, state, transition in
|
||||
self?.updatePaneDidScroll(pane: pane, state: state, transition: transition)
|
||||
}
|
||||
@ -773,7 +778,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
self.stickerSearchContainerNodeLoadedDisposable.dispose()
|
||||
self.searchContainerNodeLoadedDisposable.dispose()
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
@ -785,7 +790,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor
|
||||
self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor
|
||||
|
||||
self.stickerSearchContainerNode?.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
self.searchContainerNode?.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
|
||||
self.stickerPane.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
self.gifPane.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
@ -802,62 +807,62 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
let panes: [ASDisplayNode]
|
||||
if let stickerSearchContainerNode = strongSelf.stickerSearchContainerNode {
|
||||
if let searchContainerNode = strongSelf.searchContainerNode {
|
||||
panes = []
|
||||
if let (itemNode, item) = stickerSearchContainerNode.itemAt(point: point.offsetBy(dx: -stickerSearchContainerNode.frame.minX, dy: -stickerSearchContainerNode.frame.minY)) {
|
||||
return strongSelf.context.account.postbox.transaction { transaction -> Bool in
|
||||
return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId)
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in
|
||||
if let strongSelf = self {
|
||||
var menuItems: [PeekControllerMenuItem] = []
|
||||
menuItems = [
|
||||
PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: {
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false)
|
||||
}
|
||||
}),
|
||||
PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: {
|
||||
if let strongSelf = self {
|
||||
if isStarred {
|
||||
let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start()
|
||||
} else {
|
||||
let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start()
|
||||
}
|
||||
}
|
||||
}),
|
||||
PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: {
|
||||
if let strongSelf = self {
|
||||
loop: for attribute in item.file.attributes {
|
||||
switch attribute {
|
||||
case let .Sticker(_, packReference, _):
|
||||
if let packReference = packReference {
|
||||
let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: strongSelf.controllerInteraction.navigationController())
|
||||
controller.sendSticker = { file in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction.sendSticker(file, false)
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true)
|
||||
strongSelf.controllerInteraction.presentController(controller, nil)
|
||||
}
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {})
|
||||
]
|
||||
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// if let (itemNode, item) = searchContainerNode.itemAt(point: point.offsetBy(dx: -searchContainerNode.frame.minX, dy: -searchContainerNode.frame.minY)) {
|
||||
// return strongSelf.context.account.postbox.transaction { transaction -> Bool in
|
||||
// return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId)
|
||||
// }
|
||||
// |> deliverOnMainQueue
|
||||
// |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in
|
||||
// if let strongSelf = self {
|
||||
// var menuItems: [PeekControllerMenuItem] = []
|
||||
// menuItems = [
|
||||
// PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: {
|
||||
// if let strongSelf = self {
|
||||
// strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false)
|
||||
// }
|
||||
// }),
|
||||
// PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: {
|
||||
// if let strongSelf = self {
|
||||
// if isStarred {
|
||||
// let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start()
|
||||
// } else {
|
||||
// let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start()
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
// PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: {
|
||||
// if let strongSelf = self {
|
||||
// loop: for attribute in item.file.attributes {
|
||||
// switch attribute {
|
||||
// case let .Sticker(_, packReference, _):
|
||||
// if let packReference = packReference {
|
||||
// let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: strongSelf.controllerInteraction.navigationController())
|
||||
// controller.sendSticker = { file in
|
||||
// if let strongSelf = self {
|
||||
// strongSelf.controllerInteraction.sendSticker(file, false)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true)
|
||||
// strongSelf.controllerInteraction.presentController(controller, nil)
|
||||
// }
|
||||
// break loop
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
// PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {})
|
||||
// ]
|
||||
// return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems))
|
||||
// } else {
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
panes = [strongSelf.gifPane, strongSelf.stickerPane, strongSelf.trendingPane]
|
||||
}
|
||||
@ -1123,6 +1128,11 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, isVisible: Bool) -> (CGFloat, CGFloat) {
|
||||
var searchMode: ChatMediaInputSearchMode?
|
||||
if let (_, _, _, _, _, _, _, _, interfaceState, _) = self.validLayout, case let .media(_, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded, case let .search(mode) = expanded {
|
||||
searchMode = mode
|
||||
}
|
||||
|
||||
self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, isVisible)
|
||||
|
||||
if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings {
|
||||
@ -1130,18 +1140,19 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
var displaySearch = false
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
let panelHeight: CGFloat
|
||||
|
||||
var isExpanded: Bool = false
|
||||
if case let .media(_, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded {
|
||||
if case let .media(mode, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded {
|
||||
isExpanded = true
|
||||
switch expanded {
|
||||
case .content:
|
||||
panelHeight = maximumHeight
|
||||
case .search:
|
||||
case let .search(mode):
|
||||
panelHeight = maximumHeight
|
||||
displaySearch = true
|
||||
searchMode = mode
|
||||
}
|
||||
self.stickerPane.collectionListPanelOffset = 0.0
|
||||
self.gifPane.collectionListPanelOffset = 0.0
|
||||
@ -1152,24 +1163,32 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
if displaySearch {
|
||||
if let stickerSearchContainerNode = self.stickerSearchContainerNode {
|
||||
if let searchContainerNode = self.searchContainerNode {
|
||||
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -inputPanelHeight), size: CGSize(width: width, height: panelHeight + inputPanelHeight))
|
||||
if stickerSearchContainerNode.supernode != nil {
|
||||
transition.updateFrame(node: stickerSearchContainerNode, frame: containerFrame)
|
||||
stickerSearchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: transition)
|
||||
if searchContainerNode.supernode != nil {
|
||||
transition.updateFrame(node: searchContainerNode, frame: containerFrame)
|
||||
searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: transition)
|
||||
} else {
|
||||
self.stickerSearchContainerNode = stickerSearchContainerNode
|
||||
self.insertSubnode(stickerSearchContainerNode, belowSubnode: self.collectionListContainer)
|
||||
stickerSearchContainerNode.frame = containerFrame
|
||||
stickerSearchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: .immediate)
|
||||
var placeholderNode: StickerPaneSearchBarPlaceholderNode?
|
||||
self.stickerPane.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? StickerPaneSearchBarPlaceholderNode {
|
||||
placeholderNode = itemNode
|
||||
self.searchContainerNode = searchContainerNode
|
||||
self.insertSubnode(searchContainerNode, belowSubnode: self.collectionListContainer)
|
||||
searchContainerNode.frame = containerFrame
|
||||
searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: .immediate)
|
||||
var placeholderNode: PaneSearchBarPlaceholderNode?
|
||||
if let searchMode = searchMode {
|
||||
switch searchMode {
|
||||
case .gif:
|
||||
placeholderNode = self.gifPane.searchPlaceholderNode
|
||||
case .sticker:
|
||||
self.stickerPane.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? PaneSearchBarPlaceholderNode {
|
||||
placeholderNode = itemNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let placeholderNode = placeholderNode {
|
||||
stickerSearchContainerNode.animateIn(from: placeholderNode, transition: transition)
|
||||
searchContainerNode.animateIn(from: placeholderNode, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1227,7 +1246,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
let paneFrame = CGRect(origin: CGPoint(x: paneOrigin + leftInset, y: 0.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight))
|
||||
switch pane {
|
||||
case .gifs:
|
||||
if self.gifPane.supernode == nil {
|
||||
if self.gifPane.supernode == nil {
|
||||
self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer)
|
||||
self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 0.0), size: CGSize(width: width, height: panelHeight))
|
||||
}
|
||||
@ -1336,22 +1355,29 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.animatingTrendingPaneOut = false
|
||||
}
|
||||
|
||||
if !displaySearch, let stickerSearchContainerNode = self.stickerSearchContainerNode {
|
||||
self.stickerSearchContainerNode = nil
|
||||
self.stickerSearchContainerNodeLoadedDisposable.set(nil)
|
||||
if !displaySearch, let searchContainerNode = self.searchContainerNode {
|
||||
self.searchContainerNode = nil
|
||||
self.searchContainerNodeLoadedDisposable.set(nil)
|
||||
|
||||
var placeholderNode: StickerPaneSearchBarPlaceholderNode?
|
||||
self.stickerPane.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? StickerPaneSearchBarPlaceholderNode {
|
||||
placeholderNode = itemNode
|
||||
var placeholderNode: PaneSearchBarPlaceholderNode?
|
||||
if let searchMode = searchMode {
|
||||
switch searchMode {
|
||||
case .gif:
|
||||
placeholderNode = self.gifPane.searchPlaceholderNode
|
||||
case .sticker:
|
||||
self.stickerPane.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? PaneSearchBarPlaceholderNode {
|
||||
placeholderNode = itemNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let placeholderNode = placeholderNode {
|
||||
stickerSearchContainerNode.animateOut(to: placeholderNode, transition: transition, completion: { [weak stickerSearchContainerNode] in
|
||||
stickerSearchContainerNode?.removeFromSupernode()
|
||||
searchContainerNode.animateOut(to: placeholderNode, transition: transition, completion: { [weak searchContainerNode] in
|
||||
searchContainerNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
stickerSearchContainerNode.removeFromSupernode()
|
||||
searchContainerNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1399,7 +1425,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
}
|
||||
|
||||
self.stickerSearchContainerNode?.updatePreviewing(animated: animated)
|
||||
self.searchContainerNode?.contentNode.updatePreviewing(animated: animated)
|
||||
self.trendingPane.updatePreviewing(animated: animated)
|
||||
}
|
||||
}
|
||||
@ -1515,8 +1541,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let stickerSearchContainerNode = self.stickerSearchContainerNode {
|
||||
if let result = stickerSearchContainerNode.hitTest(point.offsetBy(dx: -stickerSearchContainerNode.frame.minX, dy: -stickerSearchContainerNode.frame.minY), with: event) {
|
||||
if let searchContainerNode = self.searchContainerNode {
|
||||
if let result = searchContainerNode.hitTest(point.offsetBy(dx: -searchContainerNode.frame.minX, dy: -searchContainerNode.frame.minY), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ struct ChatMediaInputPaneScrollState {
|
||||
}
|
||||
|
||||
class ChatMediaInputPane: ASDisplayNode {
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
var collectionListPanelOffset: CGFloat = 0.0
|
||||
|
||||
func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
@ -78,7 +78,7 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane {
|
||||
if isExpanded {
|
||||
var scrollIndex: Int?
|
||||
for i in 0 ..< self.gridNode.items.count {
|
||||
if let _ = self.gridNode.items[i] as? StickerPaneSearchBarPlaceholderItem {
|
||||
if let _ = self.gridNode.items[i] as? PaneSearchBarPlaceholderItem {
|
||||
scrollIndex = i
|
||||
break
|
||||
}
|
||||
@ -101,10 +101,6 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane {
|
||||
}
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), preloadSize: isVisible ? 300.0 : 0.0, type: .fixed(itemSize: itemSize, fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
|
||||
if false, let scrollToItem = scrollToItem {
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.gridNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
||||
|
||||
if self.isPaneVisible != isVisible {
|
||||
|
||||
@ -1194,6 +1194,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
|
||||
if let file = self.media as? TelegramMediaFile, file.isAnimated {
|
||||
isAnimated = true
|
||||
}
|
||||
|
||||
var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound
|
||||
if let message = self.message, message.id.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
actionAtEnd = .loop
|
||||
}
|
||||
|
||||
if let videoNode = self.videoNode, let context = self.context, (self.automaticPlayback ?? false) && !isAnimated {
|
||||
var isHorizontal = false
|
||||
@ -1216,7 +1221,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
if canPlay {
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none)
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: actionAtEnd)
|
||||
}
|
||||
})
|
||||
}, (self.playerStatus?.soundEnabled ?? false) && isHorizontal, false, false, self.badgeNode)
|
||||
|
||||
@ -152,9 +152,14 @@ enum ChatMediaInputMode {
|
||||
case other
|
||||
}
|
||||
|
||||
enum ChatMediaInputExpanded {
|
||||
enum ChatMediaInputSearchMode {
|
||||
case gif
|
||||
case sticker
|
||||
}
|
||||
|
||||
enum ChatMediaInputExpanded: Equatable {
|
||||
case content
|
||||
case search
|
||||
case search(ChatMediaInputSearchMode)
|
||||
}
|
||||
|
||||
enum ChatInputMode: Equatable {
|
||||
|
||||
@ -1271,11 +1271,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
@objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
var activateGifInput = false
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
if case .media(.gif, _) = presentationInterfaceState.inputMode {
|
||||
activateGifInput = true
|
||||
}
|
||||
}
|
||||
// if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
// if case .media(.gif, _) = presentationInterfaceState.inputMode {
|
||||
// activateGifInput = true
|
||||
// }
|
||||
// }
|
||||
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
||||
return (.text, state.keyboardButtonsMessage?.id)
|
||||
})
|
||||
|
||||
265
TelegramUI/GifPaneSearchContentNode.swift
Normal file
265
TelegramUI/GifPaneSearchContentNode.swift
Normal file
@ -0,0 +1,265 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
private let trendingGifsPromise = Promise<[FileMediaReference]?>(nil)
|
||||
|
||||
final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
|
||||
private var multiplexedNode: MultiplexedVideoNode?
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let searchDisposable = MetaDisposable()
|
||||
|
||||
private let _ready = Promise<Void>()
|
||||
var ready: Signal<Void, NoError> {
|
||||
return self._ready.get()
|
||||
}
|
||||
|
||||
var deactivateSearchBar: (() -> Void)?
|
||||
var updateActivity: ((Bool) -> Void)?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
super.init()
|
||||
|
||||
let _ = (trendingGifsPromise.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { next in
|
||||
if next == nil {
|
||||
trendingGifsPromise.set(self.signalForQuery(""))
|
||||
}
|
||||
})
|
||||
|
||||
self._ready.set(.single(Void()))
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.searchDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateText(_ text: String) {
|
||||
let signal: Signal<[FileMediaReference]?, NoError>
|
||||
if !text.isEmpty {
|
||||
signal = self.signalForQuery(text)
|
||||
self.updateActivity?(true)
|
||||
} else {
|
||||
signal = trendingGifsPromise.get()
|
||||
self.updateActivity?(false)
|
||||
}
|
||||
|
||||
self.searchDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self, let result = result else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.multiplexedNode?.files = result
|
||||
strongSelf.updateActivity?(false)
|
||||
}))
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
}
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
}
|
||||
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPreviewPeekItem)? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let firstLayout = self.validLayout == nil
|
||||
self.validLayout = size
|
||||
|
||||
// if let image = self.notFoundNode.image {
|
||||
// let areaHeight = size.height - inputHeight
|
||||
//
|
||||
// let labelSize = self.notFoundLabel.updateLayout(CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
//
|
||||
// transition.updateFrame(node: self.notFoundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((areaHeight - image.size.height - labelSize.height) / 2.0)), size: image.size))
|
||||
// transition.updateFrame(node: self.notFoundLabel, frame: CGRect(origin: CGPoint(x: floor((image.size.width - labelSize.width) / 2.0), y: image.size.height + 8.0), size: labelSize))
|
||||
// }
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if let multiplexedNode = self.multiplexedNode {
|
||||
multiplexedNode.topInset = 0.0
|
||||
multiplexedNode.bottomInset = 0.0
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
|
||||
transition.updateFrame(layer: multiplexedNode.layer, frame: nodeFrame)
|
||||
multiplexedNode.updateLayout(size: nodeFrame.size, transition: transition)
|
||||
}
|
||||
|
||||
if firstLayout {
|
||||
self.updateText("")
|
||||
}
|
||||
}
|
||||
|
||||
override func willEnterHierarchy() {
|
||||
super.willEnterHierarchy()
|
||||
|
||||
if self.multiplexedNode == nil {
|
||||
let multiplexedNode = MultiplexedVideoNode(account: self.context.account)
|
||||
self.multiplexedNode = multiplexedNode
|
||||
if let layout = self.validLayout {
|
||||
multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout)
|
||||
}
|
||||
|
||||
self.view.addSubview(multiplexedNode)
|
||||
|
||||
let gifs = self.context.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)])
|
||||
|> map { view -> [FileMediaReference] in
|
||||
var recentGifs: OrderedItemListView?
|
||||
if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] {
|
||||
recentGifs = orderedView as? OrderedItemListView
|
||||
}
|
||||
if let recentGifs = recentGifs {
|
||||
return recentGifs.items.map { item in
|
||||
let file = (item.contents as! RecentMediaItem).media as! TelegramMediaFile
|
||||
return .savedGif(media: file)
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
self.searchDisposable.set((gifs |> deliverOnMainQueue).start(next: { [weak self] gifs in
|
||||
if let strongSelf = self {
|
||||
let previousFiles = strongSelf.multiplexedNode?.files
|
||||
strongSelf.multiplexedNode?.files = gifs
|
||||
if (previousFiles ?? []).isEmpty {
|
||||
strongSelf.multiplexedNode?.contentOffset = CGPoint(x: 0.0, y: 60.0)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
multiplexedNode.fileSelected = { [weak self] fileReference in
|
||||
self?.controllerInteraction.sendGif(fileReference)
|
||||
}
|
||||
|
||||
multiplexedNode.didScroll = { [weak self] offset, height in
|
||||
self?.deactivateSearchBar?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(additivePosition: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let multiplexedNode = self.multiplexedNode else {
|
||||
return
|
||||
}
|
||||
|
||||
multiplexedNode.alpha = 0.0
|
||||
transition.updateAlpha(layer: multiplexedNode.layer, alpha: 1.0, completion: { _ in
|
||||
})
|
||||
|
||||
if case let .animated(duration, curve) = transition {
|
||||
multiplexedNode.layer.animatePosition(from: CGPoint(x: 0.0, y: additivePosition), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(transition: ContainedViewLayoutTransition) {
|
||||
guard let multiplexedNode = self.multiplexedNode else {
|
||||
return
|
||||
}
|
||||
|
||||
transition.updateAlpha(layer: multiplexedNode.layer, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
}
|
||||
|
||||
private func signalForQuery(_ query: String) -> Signal<[FileMediaReference]?, NoError> {
|
||||
let delayRequest = true
|
||||
|
||||
let account = self.context.account
|
||||
let contextBot = resolvePeerByName(account: account, name: "gif")
|
||||
|> mapToSignal { peerId -> Signal<Peer?, NoError> in
|
||||
if let peerId = peerId {
|
||||
return account.postbox.loadedPeerWithId(peerId)
|
||||
|> map { peer -> Peer? in
|
||||
return peer
|
||||
}
|
||||
|> take(1)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in
|
||||
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
|
||||
let results = requestContextResults(account: account, botId: user.id, query: query, peerId: self.context.account.peerId, limit: 64)
|
||||
|> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
return { _ in
|
||||
return .contextRequestResult(user, results)
|
||||
}
|
||||
}
|
||||
|
||||
let botResult: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ previousResult in
|
||||
var passthroughPreviousResult: ChatContextResultCollection?
|
||||
if let previousResult = previousResult {
|
||||
if case let .contextRequestResult(previousUser, previousResults) = previousResult {
|
||||
if previousUser?.id == user.id {
|
||||
passthroughPreviousResult = previousResults
|
||||
}
|
||||
}
|
||||
}
|
||||
return .contextRequestResult(nil, passthroughPreviousResult)
|
||||
})
|
||||
|
||||
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>
|
||||
if delayRequest {
|
||||
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
|
||||
} else {
|
||||
maybeDelayedContextResults = results
|
||||
}
|
||||
|
||||
return botResult |> then(maybeDelayedContextResults)
|
||||
} else {
|
||||
return .single({ _ in return nil })
|
||||
}
|
||||
}
|
||||
return contextBot
|
||||
|> mapToSignal { result -> Signal<[FileMediaReference]?, NoError> in
|
||||
if let r = result(nil), case let .contextRequestResult(_, collection) = r, let results = collection?.results {
|
||||
var references: [FileMediaReference] = []
|
||||
for result in results {
|
||||
switch result {
|
||||
case let .internalReference(_, _, _, _, _, _, file, _):
|
||||
if let file = file {
|
||||
references.append(FileMediaReference.standalone(media: file))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return .single(references)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|> beforeStarted { [weak self] in
|
||||
self?.updateActivity?(true)
|
||||
}
|
||||
|> afterCompleted { [weak self] in
|
||||
self?.updateActivity?(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -49,6 +49,7 @@ enum MediaPlayerActionAtEnd {
|
||||
}
|
||||
|
||||
enum MediaPlayerPlayOnceWithSoundActionAtEnd {
|
||||
case loop
|
||||
case loopDisablingSound
|
||||
case stop
|
||||
}
|
||||
|
||||
@ -269,6 +269,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
}
|
||||
}
|
||||
switch actionAtEnd {
|
||||
case .loop:
|
||||
self.player.actionAtEnd = .loop({})
|
||||
case .loopDisablingSound:
|
||||
self.player.actionAtEnd = .loopDisablingSound(action)
|
||||
case .stop:
|
||||
@ -294,6 +296,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
}
|
||||
}
|
||||
switch actionAtEnd {
|
||||
case .loop:
|
||||
self.player.actionAtEnd = .loop({})
|
||||
case .loopDisablingSound:
|
||||
self.player.actionAtEnd = .loopDisablingSound(action)
|
||||
case .stop:
|
||||
|
||||
@ -22,7 +22,7 @@ private func generateBackground(backgroundColor: UIColor, foregroundColor: UICol
|
||||
}, opaque: true)?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0))
|
||||
}
|
||||
|
||||
private class StickerPaneSearchBarTextField: UITextField {
|
||||
private class PaneSearchBarTextField: UITextField {
|
||||
public var didDeleteBackwardWhileEmpty: (() -> Void)?
|
||||
|
||||
let placeholderLabel: ImmediateTextNode
|
||||
@ -119,7 +119,7 @@ private class StickerPaneSearchBarTextField: UITextField {
|
||||
}
|
||||
}
|
||||
|
||||
class StickerPaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
var cancel: (() -> Void)?
|
||||
var textUpdated: ((String) -> Void)?
|
||||
var clearPrefix: (() -> Void)?
|
||||
@ -129,7 +129,7 @@ class StickerPaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
private let textBackgroundNode: ASImageNode
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
private let iconNode: ASImageNode
|
||||
private let textField: StickerPaneSearchBarTextField
|
||||
private let textField: PaneSearchBarTextField
|
||||
private let clearButton: HighlightableButtonNode
|
||||
private let cancelButton: ASButtonNode
|
||||
|
||||
@ -212,7 +212,7 @@ class StickerPaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
|
||||
self.textField = StickerPaneSearchBarTextField()
|
||||
self.textField = PaneSearchBarTextField()
|
||||
self.textField.autocorrectionType = .no
|
||||
self.textField.returnKeyType = .done
|
||||
self.textField.font = Font.regular(17.0)
|
||||
@ -321,8 +321,8 @@ class StickerPaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
self.textField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func animateIn(from node: StickerPaneSearchBarPlaceholderNode, duration: Double, timingFunction: String) {
|
||||
let initialTextBackgroundFrame = node.convert(node.backgroundNode.frame, to: self)
|
||||
func animateIn(from node: PaneSearchBarPlaceholderNode, duration: Double, timingFunction: String) {
|
||||
let initialTextBackgroundFrame = node.view.convert(node.backgroundNode.frame, to: self.view)
|
||||
|
||||
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, initialTextBackgroundFrame.maxY + 8.0)))
|
||||
if let fromBackgroundColor = node.backgroundColor, let toBackgroundColor = self.backgroundNode.backgroundColor {
|
||||
@ -359,7 +359,7 @@ class StickerPaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func transitionOut(to node: StickerPaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
func transitionOut(to node: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
let targetTextBackgroundFrame = node.view.convert(node.backgroundNode.view.frame, to: self.view)
|
||||
|
||||
let duration: Double = 0.5
|
||||
@ -9,39 +9,46 @@ private func generateLoupeIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: templateLoupeIcon, color: color)
|
||||
}
|
||||
|
||||
final class StickerPaneSearchBarPlaceholderItem: GridItem {
|
||||
enum PaneSearchBarType {
|
||||
case stickers
|
||||
case gifs
|
||||
}
|
||||
|
||||
final class PaneSearchBarPlaceholderItem: GridItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let type: PaneSearchBarType
|
||||
let activate: () -> Void
|
||||
|
||||
let section: GridSection? = nil
|
||||
let fillsRowWithHeight: CGFloat? = 56.0
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, activate: @escaping () -> Void) {
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType, activate: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.type = type
|
||||
self.activate = activate
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = StickerPaneSearchBarPlaceholderNode()
|
||||
let node = PaneSearchBarPlaceholderNode()
|
||||
node.activate = self.activate
|
||||
node.setup(theme: self.theme, strings: self.strings)
|
||||
node.setup(theme: self.theme, strings: self.strings, type: self.type)
|
||||
return node
|
||||
}
|
||||
|
||||
func update(node: GridItemNode) {
|
||||
guard let node = node as? StickerPaneSearchBarPlaceholderNode else {
|
||||
guard let node = node as? PaneSearchBarPlaceholderNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
node.activate = self.activate
|
||||
node.setup(theme: self.theme, strings: self.strings)
|
||||
node.setup(theme: self.theme, strings: self.strings, type: self.type)
|
||||
}
|
||||
}
|
||||
|
||||
final class StickerPaneSearchBarPlaceholderNode: GridItemNode {
|
||||
private var currentState: (PresentationTheme, PresentationStrings)?
|
||||
final class PaneSearchBarPlaceholderNode: GridItemNode {
|
||||
private var currentState: (PresentationTheme, PresentationStrings, PaneSearchBarType)?
|
||||
var activate: (() -> Void)?
|
||||
|
||||
let backgroundNode: ASImageNode
|
||||
@ -76,11 +83,20 @@ final class StickerPaneSearchBarPlaceholderNode: GridItemNode {
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
func setup(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
if self.currentState?.0 !== theme || self.currentState?.1 !== strings {
|
||||
func setup(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType) {
|
||||
if self.currentState?.0 !== theme || self.currentState?.1 !== strings || self.currentState?.2 != type {
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor)
|
||||
self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor)
|
||||
self.labelNode.attributedText = NSAttributedString(string: strings.Stickers_Search, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
|
||||
let placeholder: String
|
||||
switch type {
|
||||
case .stickers:
|
||||
placeholder = strings.Stickers_Search
|
||||
case .gifs:
|
||||
placeholder = strings.Gif_Search
|
||||
}
|
||||
self.labelNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
|
||||
|
||||
self.currentState = (theme, strings, type)
|
||||
}
|
||||
}
|
||||
|
||||
149
TelegramUI/PaneSearchContainerNode.swift
Normal file
149
TelegramUI/PaneSearchContainerNode.swift
Normal file
@ -0,0 +1,149 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
protocol PaneSearchContentNode {
|
||||
var ready: Signal<Void, NoError> { get }
|
||||
var deactivateSearchBar: (() -> Void)? { get set }
|
||||
var updateActivity: ((Bool) -> Void)? { get set }
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings)
|
||||
func updateText(_ text: String)
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, transition: ContainedViewLayoutTransition)
|
||||
|
||||
func animateIn(additivePosition: CGFloat, transition: ContainedViewLayoutTransition)
|
||||
func animateOut(transition: ContainedViewLayoutTransition)
|
||||
|
||||
func updatePreviewing(animated: Bool)
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPreviewPeekItem)?
|
||||
}
|
||||
|
||||
final class PaneSearchContainerNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private let mode: ChatMediaInputSearchMode
|
||||
public private(set) var contentNode: PaneSearchContentNode & ASDisplayNode
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let searchBar: PaneSearchBarNode
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
var ready: Signal<Void, NoError> {
|
||||
return self.contentNode.ready
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, cancel: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
switch mode {
|
||||
case .gif:
|
||||
self.contentNode = GifPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction)
|
||||
case .sticker:
|
||||
self.contentNode = StickerPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction)
|
||||
}
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
|
||||
self.searchBar = PaneSearchBarNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.contentNode)
|
||||
self.addSubnode(self.searchBar)
|
||||
|
||||
self.contentNode.deactivateSearchBar = { [weak self] in
|
||||
self?.searchBar.deactivate(clear: false)
|
||||
}
|
||||
self.contentNode.updateActivity = { [weak self] active in
|
||||
self?.searchBar.activity = active
|
||||
}
|
||||
|
||||
self.searchBar.cancel = {
|
||||
cancel()
|
||||
}
|
||||
self.searchBar.activate()
|
||||
|
||||
self.searchBar.textUpdated = { [weak self] text in
|
||||
self?.contentNode.updateText(text)
|
||||
}
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor
|
||||
self.contentNode.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
self.searchBar.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
|
||||
let placeholder: String
|
||||
switch mode {
|
||||
case .gif:
|
||||
placeholder = strings.Gif_Search
|
||||
case .sticker:
|
||||
placeholder = strings.Stickers_Search
|
||||
}
|
||||
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = size
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let searchBarHeight: CGFloat = 52.0
|
||||
transition.updateFrame(node: self.searchBar, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: searchBarHeight)))
|
||||
self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: searchBarHeight), size: CGSize(width: size.width - leftInset - rightInset, height: size.height - searchBarHeight))
|
||||
|
||||
transition.updateFrame(node: self.contentNode, frame: contentFrame)
|
||||
self.contentNode.updateLayout(size: contentFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: transition)
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
self.searchBar.deactivate(clear: true)
|
||||
}
|
||||
|
||||
func animateIn(from placeholder: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition) {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
let verticalOrigin = placeholderFrame.minY - 4.0
|
||||
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition)
|
||||
|
||||
switch transition {
|
||||
case let .animated(duration, curve):
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0)
|
||||
self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction)
|
||||
if let size = self.validLayout {
|
||||
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin)))
|
||||
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)
|
||||
}
|
||||
case .immediate:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(to placeholder: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
if case let .animated(duration, curve) = transition {
|
||||
if let size = self.validLayout {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
let verticalOrigin = placeholderFrame.minY - 4.0
|
||||
self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin))), duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
self.searchBar.transitionOut(to: placeholder, transition: transition, completion: {
|
||||
completion()
|
||||
})
|
||||
transition.updateAlpha(node: self.searchBar, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
transition.updateAlpha(node: self.backgroundNode, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
self.contentNode.animateOut(transition: transition)
|
||||
self.deactivate()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,514 +0,0 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
import LegacyComponents
|
||||
import TelegramUIPrivateModule
|
||||
|
||||
final class StickerPaneSearchInteraction {
|
||||
let open: (StickerPackCollectionInfo) -> Void
|
||||
let install: (StickerPackCollectionInfo) -> Void
|
||||
let sendSticker: (FileMediaReference) -> Void
|
||||
let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
|
||||
init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo) -> Void, sendSticker: @escaping (FileMediaReference) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) {
|
||||
self.open = open
|
||||
self.install = install
|
||||
self.sendSticker = sendSticker
|
||||
self.getItemIsPreviewed = getItemIsPreviewed
|
||||
}
|
||||
}
|
||||
|
||||
private enum StickerSearchEntryId: Equatable, Hashable {
|
||||
case sticker(String?, Int64)
|
||||
case global(ItemCollectionId)
|
||||
}
|
||||
|
||||
private enum StickerSearchEntry: Identifiable, Comparable {
|
||||
case sticker(index: Int, code: String?, stickerItem: FoundStickerItem, theme: PresentationTheme)
|
||||
case global(index: Int, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool)
|
||||
|
||||
var stableId: StickerSearchEntryId {
|
||||
switch self {
|
||||
case let .sticker(_, code, stickerItem, _):
|
||||
return .sticker(code, stickerItem.file.fileId.id)
|
||||
case let .global(_, info, _, _):
|
||||
return .global(info.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .sticker(lhsIndex, lhsCode, lhsStickerItem, lhsTheme):
|
||||
if case let .sticker(rhsIndex, rhsCode, rhsStickerItem, rhsTheme) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if lhsCode != rhsCode {
|
||||
return false
|
||||
}
|
||||
if lhsStickerItem != rhsStickerItem {
|
||||
return false
|
||||
}
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .global(index, info, topItems, installed):
|
||||
if case .global(index, info, topItems, installed) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .sticker(lhsIndex, _, _, _):
|
||||
switch rhs {
|
||||
case let .sticker(rhsIndex, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .global(lhsIndex, _, _, _):
|
||||
switch rhs {
|
||||
case .sticker:
|
||||
return false
|
||||
case let .global(rhsIndex, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem {
|
||||
switch self {
|
||||
case let .sticker(_, code, stickerItem, theme):
|
||||
return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: {
|
||||
interaction.sendSticker(.standalone(media: stickerItem.file))
|
||||
})
|
||||
case let .global(_, info, topItems, installed):
|
||||
return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, info: info, topItems: topItems, installed: installed, unread: false, open: {
|
||||
interaction.open(info)
|
||||
}, install: {
|
||||
interaction.install(info)
|
||||
}, getItemIsPreviewed: { item in
|
||||
return interaction.getItemIsPreviewed(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct StickerPaneSearchGridTransition {
|
||||
let deletions: [Int]
|
||||
let insertions: [GridNodeInsertItem]
|
||||
let updates: [GridNodeUpdateItem]
|
||||
let updateFirstIndexInSectionOffset: Int?
|
||||
let stationaryItems: GridNodeStationaryItems
|
||||
let scrollToItem: GridNodeScrollToItem?
|
||||
let animated: Bool
|
||||
}
|
||||
|
||||
private func preparedChatMediaInputGridEntryTransition(account: Account, theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [StickerSearchEntry], to toEntries: [StickerSearchEntry], interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> StickerPaneSearchGridTransition {
|
||||
let stationaryItems: GridNodeStationaryItems = .none
|
||||
let scrollToItem: GridNodeScrollToItem? = nil
|
||||
var animated = false
|
||||
animated = true
|
||||
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices
|
||||
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) }
|
||||
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction)) }
|
||||
|
||||
let firstIndexInSectionOffset = 0
|
||||
|
||||
return StickerPaneSearchGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated)
|
||||
}
|
||||
|
||||
final class StickerPaneSearchContainerNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let searchBar: StickerPaneSearchBarNode
|
||||
private let trendingPane: ChatMediaInputTrendingPane
|
||||
private let gridNode: GridNode
|
||||
private let notFoundNode: ASImageNode
|
||||
private let notFoundLabel: ImmediateTextNode
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private var enqueuedTransitions: [StickerPaneSearchGridTransition] = []
|
||||
|
||||
private let searchDisposable = MetaDisposable()
|
||||
|
||||
private let _ready = Promise<Void>()
|
||||
var ready: Signal<Void, NoError> {
|
||||
return self._ready.get()
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, cancel: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
|
||||
self.trendingPane = ChatMediaInputTrendingPane(context: context, controllerInteraction: controllerInteraction, getItemIsPreviewed: { [weak inputNodeInteraction] item in
|
||||
return inputNodeInteraction?.previewedStickerPackItem == .pack(item)
|
||||
})
|
||||
|
||||
self.searchBar = StickerPaneSearchBarNode()
|
||||
|
||||
self.gridNode = GridNode()
|
||||
|
||||
self.notFoundNode = ASImageNode()
|
||||
self.notFoundNode.displayWithoutProcessing = true
|
||||
self.notFoundNode.displaysAsynchronously = false
|
||||
self.notFoundNode.clipsToBounds = false
|
||||
|
||||
self.notFoundLabel = ImmediateTextNode()
|
||||
self.notFoundLabel.displaysAsynchronously = false
|
||||
self.notFoundLabel.isUserInteractionEnabled = false
|
||||
self.notFoundNode.addSubnode(self.notFoundLabel)
|
||||
|
||||
self.gridNode.isHidden = true
|
||||
self.trendingPane.isHidden = false
|
||||
self.notFoundNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.trendingPane)
|
||||
self.addSubnode(self.gridNode)
|
||||
self.addSubnode(self.notFoundNode)
|
||||
self.addSubnode(self.searchBar)
|
||||
|
||||
self.gridNode.scrollView.alwaysBounceVertical = true
|
||||
self.gridNode.scrollingInitiated = { [weak self] in
|
||||
self?.searchBar.deactivate(clear: false)
|
||||
}
|
||||
|
||||
self.trendingPane.scrollingInitiated = { [weak self] in
|
||||
self?.searchBar.deactivate(clear: false)
|
||||
}
|
||||
|
||||
self.searchBar.cancel = {
|
||||
cancel()
|
||||
}
|
||||
self.searchBar.activate()
|
||||
|
||||
let interaction = StickerPaneSearchInteraction(open: { [weak self] info in
|
||||
if let strongSelf = self {
|
||||
strongSelf.view.endEditing(true)
|
||||
strongSelf.controllerInteraction.presentController(StickerPackPreviewController(context: strongSelf.context, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: strongSelf.controllerInteraction.navigationController()), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
}, install: { [weak self] info in
|
||||
if let strongSelf = self {
|
||||
let _ = (loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false)
|
||||
|> mapToSignal { result -> Signal<Void, NoError> in
|
||||
switch result {
|
||||
case let .result(info, items, installed):
|
||||
if installed {
|
||||
return .complete()
|
||||
} else {
|
||||
return addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items)
|
||||
}
|
||||
case .fetching:
|
||||
break
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
return .complete()
|
||||
}).start()
|
||||
}
|
||||
}, sendSticker: { [weak self] file in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction.sendSticker(file, false)
|
||||
}
|
||||
}, getItemIsPreviewed: { item in
|
||||
return inputNodeInteraction.previewedStickerPackItem == .pack(item)
|
||||
})
|
||||
|
||||
let queue = Queue()
|
||||
let currentEntries = Atomic<[StickerSearchEntry]?>(value: nil)
|
||||
let currentRemotePacks = Atomic<FoundStickerSets?>(value: nil)
|
||||
|
||||
self.searchBar.textUpdated = { [weak self] text in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let signal: Signal<([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)?, NoError>
|
||||
if !text.isEmpty {
|
||||
let stickers: Signal<[(String?, FoundStickerItem)], NoError> = Signal { subscriber in
|
||||
var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = []
|
||||
|
||||
if text.isSingleEmoji {
|
||||
signals.append(searchStickers(account: context.account, query: text.firstEmoji)
|
||||
|> take(1)
|
||||
|> map { (nil, $0) })
|
||||
} else {
|
||||
for entry in TGEmojiSuggestions.suggestions(forQuery: text.lowercased()) {
|
||||
if let entry = entry as? TGAlphacodeEntry {
|
||||
signals.append(searchStickers(account: context.account, query: entry.emoji)
|
||||
|> take(1)
|
||||
|> map { (entry.emoji, $0) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return combineLatest(signals).start(next: { results in
|
||||
var result: [(String?, FoundStickerItem)] = []
|
||||
for (emoji, stickers) in results {
|
||||
for sticker in stickers {
|
||||
result.append((emoji, sticker))
|
||||
}
|
||||
}
|
||||
subscriber.putNext(result)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
}
|
||||
|
||||
let local = searchStickerSets(postbox: context.account.postbox, query: text)
|
||||
let remote = searchStickerSetsRemotely(network: context.account.network, query: text)
|
||||
|> delay(0.2, queue: Queue.mainQueue())
|
||||
let packs = local
|
||||
|> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in
|
||||
var localResult = result
|
||||
if let currentRemote = currentRemotePacks.with ({ $0 }) {
|
||||
localResult = localResult.merge(with: currentRemote)
|
||||
}
|
||||
return .single((localResult, false, nil))
|
||||
|> then(remote |> map { remote -> (FoundStickerSets, Bool, FoundStickerSets?) in
|
||||
return (result.merge(with: remote), true, remote)
|
||||
})
|
||||
}
|
||||
signal = combineLatest(stickers, packs)
|
||||
|> map { stickers, packs -> ([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)? in
|
||||
return (stickers, packs.0, packs.1, packs.2)
|
||||
}
|
||||
strongSelf.searchBar.activity = true
|
||||
} else {
|
||||
signal = .single(nil)
|
||||
strongSelf.searchBar.activity = false
|
||||
}
|
||||
|
||||
strongSelf.searchDisposable.set((signal
|
||||
|> deliverOn(queue)).start(next: { result in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var entries: [StickerSearchEntry] = []
|
||||
if let (stickers, packs, final, remote) = result {
|
||||
if let remote = remote {
|
||||
let _ = currentRemotePacks.swap(remote)
|
||||
}
|
||||
strongSelf.gridNode.isHidden = false
|
||||
strongSelf.trendingPane.isHidden = true
|
||||
|
||||
if final {
|
||||
strongSelf.searchBar.activity = false
|
||||
}
|
||||
|
||||
var index = 0
|
||||
for (code, sticker) in stickers {
|
||||
entries.append(.sticker(index: index, code: code, stickerItem: sticker, theme: theme))
|
||||
index += 1
|
||||
}
|
||||
for (collectionId, info, _, installed) in packs.infos {
|
||||
if let info = info as? StickerPackCollectionInfo {
|
||||
var topItems: [StickerPackItem] = []
|
||||
for e in packs.entries {
|
||||
if let item = e.item as? StickerPackItem {
|
||||
if e.index.collectionId == collectionId {
|
||||
topItems.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
entries.append(.global(index: index, info: info, topItems: topItems, installed: installed))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
if final || !entries.isEmpty {
|
||||
strongSelf.notFoundNode.isHidden = !entries.isEmpty
|
||||
}
|
||||
} else {
|
||||
let _ = currentRemotePacks.swap(nil)
|
||||
strongSelf.searchBar.activity = false
|
||||
strongSelf.gridNode.isHidden = true
|
||||
strongSelf.notFoundNode.isHidden = true
|
||||
strongSelf.trendingPane.isHidden = false
|
||||
}
|
||||
|
||||
let previousEntries = currentEntries.swap(entries)
|
||||
let transition = preparedChatMediaInputGridEntryTransition(account: context.account, theme: theme, strings: strings, from: previousEntries ?? [], to: entries, interaction: interaction, inputNodeInteraction: strongSelf.inputNodeInteraction)
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
self._ready.set(self.trendingPane.ready)
|
||||
self.trendingPane.activate()
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.searchDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor
|
||||
self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/StickersNotFoundIcon"), color: theme.list.freeMonoIcon)
|
||||
self.notFoundLabel.attributedText = NSAttributedString(string: strings.Stickers_NoStickersFound, font: Font.medium(14.0), textColor: theme.list.freeTextColor)
|
||||
self.searchBar.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
self.searchBar.placeholderString = NSAttributedString(string: strings.Stickers_Search, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let firstLayout = self.validLayout == nil
|
||||
self.validLayout = size
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let searchBarHeight: CGFloat = 52.0
|
||||
transition.updateFrame(node: self.searchBar, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: searchBarHeight)))
|
||||
self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||
|
||||
if let image = self.notFoundNode.image {
|
||||
let areaHeight = size.height - searchBarHeight - inputHeight
|
||||
|
||||
let labelSize = self.notFoundLabel.updateLayout(CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
transition.updateFrame(node: self.notFoundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: searchBarHeight + floor((areaHeight - image.size.height - labelSize.height) / 2.0)), size: image.size))
|
||||
transition.updateFrame(node: self.notFoundLabel, frame: CGRect(origin: CGPoint(x: floor((image.size.width - labelSize.width) / 2.0), y: image.size.height + 8.0), size: labelSize))
|
||||
}
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: searchBarHeight), size: CGSize(width: size.width - leftInset - rightInset, height: size.height - searchBarHeight))
|
||||
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0 + bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
|
||||
transition.updateFrame(node: self.trendingPane, frame: contentFrame)
|
||||
self.trendingPane.updateLayout(size: contentFrame.size, topInset: 0.0, bottomInset: bottomInset, isExpanded: false, isVisible: true, transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.gridNode, frame: contentFrame)
|
||||
if firstLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
self.searchBar.deactivate(clear: true)
|
||||
}
|
||||
|
||||
func animateIn(from placeholder: StickerPaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition) {
|
||||
self.gridNode.alpha = 0.0
|
||||
transition.updateAlpha(node: self.gridNode, alpha: 1.0, completion: { _ in
|
||||
})
|
||||
self.trendingPane.alpha = 0.0
|
||||
transition.updateAlpha(node: self.trendingPane, alpha: 1.0, completion: { _ in
|
||||
})
|
||||
switch transition {
|
||||
case let .animated(duration, curve):
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0)
|
||||
self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction)
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
if let size = self.validLayout {
|
||||
let verticalOrigin = placeholderFrame.minY - 4.0
|
||||
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin)))
|
||||
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)
|
||||
self.trendingPane.layer.animatePosition(from: CGPoint(x: 0.0, y: initialBackgroundFrame.minY - self.backgroundNode.frame.minY), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, additive: true)
|
||||
}
|
||||
case .immediate:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(to placeholder: StickerPaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
if case let .animated(duration, curve) = transition {
|
||||
if let size = self.validLayout {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
let verticalOrigin = placeholderFrame.minY - 4.0
|
||||
self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin))), duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
self.searchBar.transitionOut(to: placeholder, transition: transition, completion: {
|
||||
completion()
|
||||
})
|
||||
transition.updateAlpha(node: self.searchBar, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
transition.updateAlpha(node: self.backgroundNode, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
transition.updateAlpha(node: self.gridNode, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
transition.updateAlpha(node: self.trendingPane, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
transition.updateAlpha(node: self.notFoundNode, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
self.deactivate()
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: StickerPaneSearchGridTransition) {
|
||||
enqueuedTransitions.append(transition)
|
||||
|
||||
if self.validLayout != nil {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let transition = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
let itemTransition: ContainedViewLayoutTransition = .immediate
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
self.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? StickerPaneSearchStickerItemNode {
|
||||
itemNode.updatePreviewing(animated: animated)
|
||||
} else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
|
||||
itemNode.updatePreviewing(animated: animated)
|
||||
}
|
||||
}
|
||||
self.trendingPane.updatePreviewing(animated: animated)
|
||||
}
|
||||
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPreviewPeekItem)? {
|
||||
if !self.trendingPane.isHidden {
|
||||
if let (itemNode, item) = self.trendingPane.itemAt(point: self.view.convert(point, to: self.trendingPane.view)) {
|
||||
return (itemNode, .pack(item))
|
||||
}
|
||||
} else {
|
||||
if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) {
|
||||
if let itemNode = itemNode as? StickerPaneSearchStickerItemNode, let stickerItem = itemNode.stickerItem {
|
||||
return (itemNode, .found(stickerItem))
|
||||
} else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
|
||||
if let (node, item) = itemNode.itemAt(point: self.view.convert(point, to: itemNode.view)) {
|
||||
return (node, .pack(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
477
TelegramUI/StickerPaneSearchContentNode.swift
Normal file
477
TelegramUI/StickerPaneSearchContentNode.swift
Normal file
@ -0,0 +1,477 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
import LegacyComponents
|
||||
import TelegramUIPrivateModule
|
||||
|
||||
final class StickerPaneSearchInteraction {
|
||||
let open: (StickerPackCollectionInfo) -> Void
|
||||
let install: (StickerPackCollectionInfo) -> Void
|
||||
let sendSticker: (FileMediaReference) -> Void
|
||||
let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
|
||||
init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo) -> Void, sendSticker: @escaping (FileMediaReference) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) {
|
||||
self.open = open
|
||||
self.install = install
|
||||
self.sendSticker = sendSticker
|
||||
self.getItemIsPreviewed = getItemIsPreviewed
|
||||
}
|
||||
}
|
||||
|
||||
private enum StickerSearchEntryId: Equatable, Hashable {
|
||||
case sticker(String?, Int64)
|
||||
case global(ItemCollectionId)
|
||||
}
|
||||
|
||||
private enum StickerSearchEntry: Identifiable, Comparable {
|
||||
case sticker(index: Int, code: String?, stickerItem: FoundStickerItem, theme: PresentationTheme)
|
||||
case global(index: Int, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool)
|
||||
|
||||
var stableId: StickerSearchEntryId {
|
||||
switch self {
|
||||
case let .sticker(_, code, stickerItem, _):
|
||||
return .sticker(code, stickerItem.file.fileId.id)
|
||||
case let .global(_, info, _, _):
|
||||
return .global(info.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .sticker(lhsIndex, lhsCode, lhsStickerItem, lhsTheme):
|
||||
if case let .sticker(rhsIndex, rhsCode, rhsStickerItem, rhsTheme) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if lhsCode != rhsCode {
|
||||
return false
|
||||
}
|
||||
if lhsStickerItem != rhsStickerItem {
|
||||
return false
|
||||
}
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .global(index, info, topItems, installed):
|
||||
if case .global(index, info, topItems, installed) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .sticker(lhsIndex, _, _, _):
|
||||
switch rhs {
|
||||
case let .sticker(rhsIndex, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .global(lhsIndex, _, _, _):
|
||||
switch rhs {
|
||||
case .sticker:
|
||||
return false
|
||||
case let .global(rhsIndex, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem {
|
||||
switch self {
|
||||
case let .sticker(_, code, stickerItem, theme):
|
||||
return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: {
|
||||
interaction.sendSticker(.standalone(media: stickerItem.file))
|
||||
})
|
||||
case let .global(_, info, topItems, installed):
|
||||
return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, info: info, topItems: topItems, installed: installed, unread: false, open: {
|
||||
interaction.open(info)
|
||||
}, install: {
|
||||
interaction.install(info)
|
||||
}, getItemIsPreviewed: { item in
|
||||
return interaction.getItemIsPreviewed(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct StickerPaneSearchGridTransition {
|
||||
let deletions: [Int]
|
||||
let insertions: [GridNodeInsertItem]
|
||||
let updates: [GridNodeUpdateItem]
|
||||
let updateFirstIndexInSectionOffset: Int?
|
||||
let stationaryItems: GridNodeStationaryItems
|
||||
let scrollToItem: GridNodeScrollToItem?
|
||||
let animated: Bool
|
||||
}
|
||||
|
||||
private func preparedChatMediaInputGridEntryTransition(account: Account, theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [StickerSearchEntry], to toEntries: [StickerSearchEntry], interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> StickerPaneSearchGridTransition {
|
||||
let stationaryItems: GridNodeStationaryItems = .none
|
||||
let scrollToItem: GridNodeScrollToItem? = nil
|
||||
var animated = false
|
||||
animated = true
|
||||
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices
|
||||
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) }
|
||||
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction)) }
|
||||
|
||||
let firstIndexInSectionOffset = 0
|
||||
|
||||
return StickerPaneSearchGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated)
|
||||
}
|
||||
|
||||
final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
private var interaction: StickerPaneSearchInteraction?
|
||||
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
|
||||
private let trendingPane: ChatMediaInputTrendingPane
|
||||
private let gridNode: GridNode
|
||||
private let notFoundNode: ASImageNode
|
||||
private let notFoundLabel: ImmediateTextNode
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private var enqueuedTransitions: [StickerPaneSearchGridTransition] = []
|
||||
|
||||
private let searchDisposable = MetaDisposable()
|
||||
|
||||
private let queue = Queue()
|
||||
private let currentEntries = Atomic<[StickerSearchEntry]?>(value: nil)
|
||||
private let currentRemotePacks = Atomic<FoundStickerSets?>(value: nil)
|
||||
|
||||
private let _ready = Promise<Void>()
|
||||
var ready: Signal<Void, NoError> {
|
||||
return self._ready.get()
|
||||
}
|
||||
|
||||
var deactivateSearchBar: (() -> Void)?
|
||||
var updateActivity: ((Bool) -> Void)?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.trendingPane = ChatMediaInputTrendingPane(context: context, controllerInteraction: controllerInteraction, getItemIsPreviewed: { [weak inputNodeInteraction] item in
|
||||
return inputNodeInteraction?.previewedStickerPackItem == .pack(item)
|
||||
})
|
||||
|
||||
self.gridNode = GridNode()
|
||||
|
||||
self.notFoundNode = ASImageNode()
|
||||
self.notFoundNode.displayWithoutProcessing = true
|
||||
self.notFoundNode.displaysAsynchronously = false
|
||||
self.notFoundNode.clipsToBounds = false
|
||||
|
||||
self.notFoundLabel = ImmediateTextNode()
|
||||
self.notFoundLabel.displaysAsynchronously = false
|
||||
self.notFoundLabel.isUserInteractionEnabled = false
|
||||
self.notFoundNode.addSubnode(self.notFoundLabel)
|
||||
|
||||
self.gridNode.isHidden = true
|
||||
self.trendingPane.isHidden = false
|
||||
self.notFoundNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.trendingPane)
|
||||
self.addSubnode(self.gridNode)
|
||||
self.addSubnode(self.notFoundNode)
|
||||
|
||||
self.gridNode.scrollView.alwaysBounceVertical = true
|
||||
self.gridNode.scrollingInitiated = { [weak self] in
|
||||
self?.deactivateSearchBar?()
|
||||
}
|
||||
|
||||
self.trendingPane.scrollingInitiated = { [weak self] in
|
||||
self?.deactivateSearchBar?()
|
||||
}
|
||||
|
||||
self.interaction = StickerPaneSearchInteraction(open: { [weak self] info in
|
||||
if let strongSelf = self {
|
||||
strongSelf.view.endEditing(true)
|
||||
strongSelf.controllerInteraction.presentController(StickerPackPreviewController(context: strongSelf.context, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: strongSelf.controllerInteraction.navigationController()), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
}, install: { [weak self] info in
|
||||
if let strongSelf = self {
|
||||
let _ = (loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false)
|
||||
|> mapToSignal { result -> Signal<Void, NoError> in
|
||||
switch result {
|
||||
case let .result(info, items, installed):
|
||||
if installed {
|
||||
return .complete()
|
||||
} else {
|
||||
return addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items)
|
||||
}
|
||||
case .fetching:
|
||||
break
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
return .complete()
|
||||
}).start()
|
||||
}
|
||||
}, sendSticker: { [weak self] file in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction.sendSticker(file, false)
|
||||
}
|
||||
}, getItemIsPreviewed: { item in
|
||||
return inputNodeInteraction.previewedStickerPackItem == .pack(item)
|
||||
})
|
||||
|
||||
self._ready.set(self.trendingPane.ready)
|
||||
self.trendingPane.activate()
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.searchDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateText(_ text: String) {
|
||||
let signal: Signal<([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)?, NoError>
|
||||
if !text.isEmpty {
|
||||
let stickers: Signal<[(String?, FoundStickerItem)], NoError> = Signal { subscriber in
|
||||
var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = []
|
||||
|
||||
if text.isSingleEmoji {
|
||||
signals.append(searchStickers(account: self.context.account, query: text.firstEmoji)
|
||||
|> take(1)
|
||||
|> map { (nil, $0) })
|
||||
} else {
|
||||
for entry in TGEmojiSuggestions.suggestions(forQuery: text.lowercased()) {
|
||||
if let entry = entry as? TGAlphacodeEntry {
|
||||
signals.append(searchStickers(account: self.context.account, query: entry.emoji)
|
||||
|> take(1)
|
||||
|> map { (entry.emoji, $0) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return combineLatest(signals).start(next: { results in
|
||||
var result: [(String?, FoundStickerItem)] = []
|
||||
for (emoji, stickers) in results {
|
||||
for sticker in stickers {
|
||||
result.append((emoji, sticker))
|
||||
}
|
||||
}
|
||||
subscriber.putNext(result)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
}
|
||||
|
||||
let local = searchStickerSets(postbox: context.account.postbox, query: text)
|
||||
let remote = searchStickerSetsRemotely(network: context.account.network, query: text)
|
||||
|> delay(0.2, queue: Queue.mainQueue())
|
||||
let packs = local
|
||||
|> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in
|
||||
var localResult = result
|
||||
if let currentRemote = self.currentRemotePacks.with ({ $0 }) {
|
||||
localResult = localResult.merge(with: currentRemote)
|
||||
}
|
||||
return .single((localResult, false, nil))
|
||||
|> then(remote |> map { remote -> (FoundStickerSets, Bool, FoundStickerSets?) in
|
||||
return (result.merge(with: remote), true, remote)
|
||||
})
|
||||
}
|
||||
signal = combineLatest(stickers, packs)
|
||||
|> map { stickers, packs -> ([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)? in
|
||||
return (stickers, packs.0, packs.1, packs.2)
|
||||
}
|
||||
self.updateActivity?(true)
|
||||
} else {
|
||||
signal = .single(nil)
|
||||
self.updateActivity?(false)
|
||||
}
|
||||
|
||||
self.searchDisposable.set((signal
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] result in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self, let interaction = strongSelf.interaction else {
|
||||
return
|
||||
}
|
||||
|
||||
var entries: [StickerSearchEntry] = []
|
||||
if let (stickers, packs, final, remote) = result {
|
||||
if let remote = remote {
|
||||
let _ = strongSelf.currentRemotePacks.swap(remote)
|
||||
}
|
||||
strongSelf.gridNode.isHidden = false
|
||||
strongSelf.trendingPane.isHidden = true
|
||||
|
||||
if final {
|
||||
strongSelf.updateActivity?(false)
|
||||
}
|
||||
|
||||
var index = 0
|
||||
var existingStickerIds = Set<MediaId>()
|
||||
var previousCode: String?
|
||||
for (code, sticker) in stickers {
|
||||
if let id = sticker.file.id, !existingStickerIds.contains(id) {
|
||||
entries.append(.sticker(index: index, code: code != previousCode ? code : nil, stickerItem: sticker, theme: strongSelf.theme))
|
||||
index += 1
|
||||
|
||||
previousCode = code
|
||||
existingStickerIds.insert(id)
|
||||
}
|
||||
}
|
||||
for (collectionId, info, _, installed) in packs.infos {
|
||||
if let info = info as? StickerPackCollectionInfo {
|
||||
var topItems: [StickerPackItem] = []
|
||||
for e in packs.entries {
|
||||
if let item = e.item as? StickerPackItem {
|
||||
if e.index.collectionId == collectionId {
|
||||
topItems.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
entries.append(.global(index: index, info: info, topItems: topItems, installed: installed))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
if final || !entries.isEmpty {
|
||||
strongSelf.notFoundNode.isHidden = !entries.isEmpty
|
||||
}
|
||||
} else {
|
||||
let _ = strongSelf.currentRemotePacks.swap(nil)
|
||||
strongSelf.updateActivity?(false)
|
||||
strongSelf.gridNode.isHidden = true
|
||||
strongSelf.notFoundNode.isHidden = true
|
||||
strongSelf.trendingPane.isHidden = false
|
||||
}
|
||||
|
||||
let previousEntries = strongSelf.currentEntries.swap(entries)
|
||||
let transition = preparedChatMediaInputGridEntryTransition(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, from: previousEntries ?? [], to: entries, interaction: interaction, inputNodeInteraction: strongSelf.inputNodeInteraction)
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/StickersNotFoundIcon"), color: theme.list.freeMonoIcon)
|
||||
self.notFoundLabel.attributedText = NSAttributedString(string: strings.Stickers_NoStickersFound, font: Font.medium(14.0), textColor: theme.list.freeTextColor)
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: StickerPaneSearchGridTransition) {
|
||||
self.enqueuedTransitions.append(transition)
|
||||
|
||||
if self.validLayout != nil {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let transition = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
let itemTransition: ContainedViewLayoutTransition = .immediate
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
self.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? StickerPaneSearchStickerItemNode {
|
||||
itemNode.updatePreviewing(animated: animated)
|
||||
} else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
|
||||
itemNode.updatePreviewing(animated: animated)
|
||||
}
|
||||
}
|
||||
self.trendingPane.updatePreviewing(animated: animated)
|
||||
}
|
||||
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPreviewPeekItem)? {
|
||||
if !self.trendingPane.isHidden {
|
||||
if let (itemNode, item) = self.trendingPane.itemAt(point: self.view.convert(point, to: self.trendingPane.view)) {
|
||||
return (itemNode, .pack(item))
|
||||
}
|
||||
} else {
|
||||
if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) {
|
||||
if let itemNode = itemNode as? StickerPaneSearchStickerItemNode, let stickerItem = itemNode.stickerItem {
|
||||
return (itemNode, .found(stickerItem))
|
||||
} else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
|
||||
if let (node, item) = itemNode.itemAt(point: self.view.convert(point, to: itemNode.view)) {
|
||||
return (node, .pack(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let firstLayout = self.validLayout == nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
if let image = self.notFoundNode.image {
|
||||
let areaHeight = size.height - inputHeight
|
||||
|
||||
let labelSize = self.notFoundLabel.updateLayout(CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
transition.updateFrame(node: self.notFoundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((areaHeight - image.size.height - labelSize.height) / 2.0)), size: image.size))
|
||||
transition.updateFrame(node: self.notFoundLabel, frame: CGRect(origin: CGPoint(x: floor((image.size.width - labelSize.width) / 2.0), y: image.size.height + 8.0), size: labelSize))
|
||||
}
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(), size: size)
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0 + bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
|
||||
transition.updateFrame(node: self.trendingPane, frame: contentFrame)
|
||||
self.trendingPane.updateLayout(size: contentFrame.size, topInset: 0.0, bottomInset: bottomInset, isExpanded: false, isVisible: true, transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.gridNode, frame: contentFrame)
|
||||
if firstLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(additivePosition: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.gridNode.alpha = 0.0
|
||||
transition.updateAlpha(node: self.gridNode, alpha: 1.0, completion: { _ in
|
||||
})
|
||||
self.trendingPane.alpha = 0.0
|
||||
transition.updateAlpha(node: self.trendingPane, alpha: 1.0, completion: { _ in
|
||||
})
|
||||
|
||||
if case let .animated(duration, curve) = transition {
|
||||
self.trendingPane.layer.animatePosition(from: CGPoint(x: 0.0, y: additivePosition), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(transition: ContainedViewLayoutTransition) {
|
||||
transition.updateAlpha(node: self.gridNode, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
transition.updateAlpha(node: self.trendingPane, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
transition.updateAlpha(node: self.notFoundNode, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -73,19 +73,14 @@ final class StickerPaneSearchStickerItem: GridItem {
|
||||
self.stickerItem = stickerItem
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.selected = selected
|
||||
if let code = code {
|
||||
self.code = code
|
||||
self.section = StickerPaneSearchStickerSection(code: code, theme: theme)
|
||||
} else {
|
||||
self.code = nil
|
||||
self.section = nil
|
||||
}
|
||||
self.code = code
|
||||
self.section = nil
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = StickerPaneSearchStickerItemNode()
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.setup(account: self.account, stickerItem: self.stickerItem)
|
||||
node.setup(account: self.account, stickerItem: self.stickerItem, code: self.code)
|
||||
node.selected = self.selected
|
||||
return node
|
||||
}
|
||||
@ -96,14 +91,17 @@ final class StickerPaneSearchStickerItem: GridItem {
|
||||
return
|
||||
}
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.setup(account: self.account, stickerItem: self.stickerItem)
|
||||
node.setup(account: self.account, stickerItem: self.stickerItem, code: self.code)
|
||||
node.selected = self.selected
|
||||
}
|
||||
}
|
||||
|
||||
private let textFont = Font.regular(20.0)
|
||||
|
||||
final class StickerPaneSearchStickerItemNode: GridItemNode {
|
||||
private var currentState: (Account, FoundStickerItem, CGSize)?
|
||||
private let imageNode: TransformImageNode
|
||||
private let textNode: ASTextNode
|
||||
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
|
||||
@ -118,14 +116,18 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
||||
|
||||
override init() {
|
||||
self.imageNode = TransformImageNode()
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
}
|
||||
|
||||
deinit {
|
||||
stickerFetchedDisposable.dispose()
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -134,8 +136,10 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
||||
self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
|
||||
}
|
||||
|
||||
func setup(account: Account, stickerItem: FoundStickerItem) {
|
||||
func setup(account: Account, stickerItem: FoundStickerItem, code: String?) {
|
||||
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem {
|
||||
self.textNode.attributedText = NSAttributedString(string: code ?? "", font: textFont, textColor: .black)
|
||||
|
||||
if let dimensions = stickerItem.file.dimensions {
|
||||
self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start())
|
||||
@ -155,7 +159,12 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
||||
if let (_, _, mediaDimensions) = self.currentState {
|
||||
let imageSize = mediaDimensions.aspectFitted(boundingSize)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
self.imageNode.frame = imageFrame
|
||||
|
||||
let textSize = self.textNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: bounds.size.width - textSize.width, y: bounds.size.height - textSize.height), size: textSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user