New GIF search design

This commit is contained in:
Ilya Laktyushin 2019-03-09 18:52:31 +03:00
parent a95da7736f
commit 4bc196d6db
22 changed files with 3170 additions and 2650 deletions

View File

@ -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 */,

View File

@ -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

View File

@ -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:

View File

@ -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)
}
}
}
}

View File

@ -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: {

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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)
})

View 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)
}
}
}

View File

@ -49,6 +49,7 @@ enum MediaPlayerActionAtEnd {
}
enum MediaPlayerPlayOnceWithSoundActionAtEnd {
case loop
case loopDisablingSound
case stop
}

View File

@ -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:

View File

@ -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

View File

@ -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)
}
}

View 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

View File

@ -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
}
}

View 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
})
}
}

View File

@ -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)
}
}