From 2ab830e3a155e184c60f36b051b5ed16dd5acdaf Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 22 May 2020 19:13:47 +0400 Subject: [PATCH] GIF-related improvements --- build-system/xcode_version | 2 +- .../Sources/ActivityIndicator.swift | 6 +- .../ContainedViewLayoutTransition.swift | 4 +- .../Items/UniversalVideoGalleryItem.swift | 2 +- .../Sources/RequestChatContextResults.swift | 17 +- .../ChatContextResultPeekContentNode.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 35 +- .../Sources/ChatControllerNode.swift | 2 +- .../ChatInterfaceInputContextPanels.swift | 10 +- .../Sources/ChatMediaInputGifPane.swift | 82 ++- .../Sources/ChatMediaInputGridEntries.swift | 2 +- .../Sources/ChatMediaInputNode.swift | 34 +- .../Sources/ChatMediaInputTrendingPane.swift | 2 +- .../ChatMessageInteractiveMediaNode.swift | 3 +- .../ChatPanelInterfaceInteraction.swift | 4 +- .../Sources/ChatTextInputPanelNode.swift | 69 ++- .../EmojisChatInputContextPanelNode.swift | 2 +- .../Sources/FeaturedStickersScreen.swift | 2 +- .../Sources/GifPaneSearchContentNode.swift | 126 ++++- ...textResultsChatInputContextPanelNode.swift | 5 +- ...ListContextResultsChatInputPanelItem.swift | 16 +- .../Sources/HorizontalStickerGridItem.swift | 29 +- ...rizontalStickersChatContextPanelNode.swift | 6 +- .../Sources/InlineReactionSearchPanel.swift | 388 ++++++++++++++ .../Sources/MultiplexedVideoNode.swift | 479 ++++++++++++++---- .../Sources/PaneSearchBarNode.swift | 7 +- .../Sources/PaneSearchContainerNode.swift | 30 +- .../Panes/PeerInfoVisualMediaPaneNode.swift | 8 +- .../SoftwareVideoLayerFrameManager.swift | 2 +- .../Sources/SoftwareVideoThumbnailLayer.swift | 4 +- 30 files changed, 1144 insertions(+), 236 deletions(-) create mode 100644 submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift diff --git a/build-system/xcode_version b/build-system/xcode_version index a6e5b12f92..8204473ef6 100644 --- a/build-system/xcode_version +++ b/build-system/xcode_version @@ -1 +1 @@ -11.4.1 +11.5 diff --git a/submodules/ActivityIndicator/Sources/ActivityIndicator.swift b/submodules/ActivityIndicator/Sources/ActivityIndicator.swift index 0336241068..94018a3c16 100644 --- a/submodules/ActivityIndicator/Sources/ActivityIndicator.swift +++ b/submodules/ActivityIndicator/Sources/ActivityIndicator.swift @@ -114,11 +114,13 @@ public final class ActivityIndicator: ASDisplayNode { override public func didLoad() { super.didLoad() - let indicatorView = UIActivityIndicatorView(style: .whiteLarge) + let indicatorView: UIActivityIndicatorView switch self.type { case let .navigationAccent(color): + indicatorView = UIActivityIndicatorView(style: .whiteLarge) indicatorView.color = color - case let .custom(color, _, _, forceCustom): + case let .custom(color, diameter, _, forceCustom): + indicatorView = UIActivityIndicatorView(style: diameter < 15.0 ? .white : .whiteLarge) indicatorView.color = convertIndicatorColor(color) if !forceCustom { self.view.addSubview(indicatorView) diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index ec5b1a9ca0..2fa1b6bf39 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -433,8 +433,8 @@ public extension ContainedViewLayoutTransition { } } - func updateAlpha(node: ASDisplayNode, alpha: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { - if node.alpha.isEqual(to: alpha) { + func updateAlpha(node: ASDisplayNode, alpha: CGFloat, beginWithCurrentState: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if node.alpha.isEqual(to: alpha) && !force { if let completion = completion { completion(true) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index e603a96776..bf7551c22a 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -489,7 +489,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.playOnContentOwnership = false strongSelf.initiallyActivated = true strongSelf.skipInitialPause = true - strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop) + strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: isAnimated ? .loop : .stop) } } } diff --git a/submodules/TelegramCore/Sources/RequestChatContextResults.swift b/submodules/TelegramCore/Sources/RequestChatContextResults.swift index b7033ded1e..5008d2dbd6 100644 --- a/submodules/TelegramCore/Sources/RequestChatContextResults.swift +++ b/submodules/TelegramCore/Sources/RequestChatContextResults.swift @@ -12,17 +12,21 @@ public enum RequestChatContextResultsError { public final class CachedChatContextResult: PostboxCoding { public let data: Data + public let timestamp: Int32 - public init(data: Data) { + public init(data: Data, timestamp: Int32) { self.data = data + self.timestamp = timestamp } public init(decoder: PostboxDecoder) { self.data = decoder.decodeDataForKey("data") ?? Data() + self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeData(self.data, forKey: "data") + encoder.encodeInt32(self.timestamp, forKey: "timestamp") } } @@ -35,7 +39,7 @@ private struct RequestData: Codable { let query: String } -private let requestVersion = "1" +private let requestVersion = "3" public func requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String) -> Signal { return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in @@ -69,7 +73,10 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P let key = ValueBoxKey(MemoryBuffer(data: keyData)) if let cachedEntry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key)) as? CachedChatContextResult { if let cachedResult = try? JSONDecoder().decode(ChatContextResultCollection.self, from: cachedEntry.data) { - return .single(cachedResult) + let timestamp = Int32(Date().timeIntervalSince1970) + if cachedEntry.timestamp + cachedResult.cacheTimeout > timestamp { + return .single(cachedResult) + } } } } @@ -102,12 +109,12 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P } return account.postbox.transaction { transaction -> ChatContextResultCollection? in - if result.cacheTimeout > 10 { + if result.cacheTimeout > 10 && offset.isEmpty { if let resultData = try? JSONEncoder().encode(result) { let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query) if let keyData = try? JSONEncoder().encode(requestData) { let key = ValueBoxKey(MemoryBuffer(data: keyData)) - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key), entry: CachedChatContextResult(data: resultData), collectionSpec: collectionSpec) + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key), entry: CachedChatContextResult(data: resultData, timestamp: Int32(Date().timeIntervalSince1970)), collectionSpec: collectionSpec) } } } diff --git a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift index 83aff1af8c..a0af5b84c8 100644 --- a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift @@ -253,7 +253,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont } if let videoFileReference = videoFileReference { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: videoFileReference) + let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: videoFileReference, synchronousLoad: false) self.layer.addSublayer(thumbnailLayer) let layerHolder = takeSampleBufferLayer() layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index fbcb3dd245..c340e6d1b4 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -203,6 +203,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let searching = ValuePromise(false, ignoreRepeated: true) private let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() private let loadingMessage = ValuePromise(false, ignoreRepeated: true) + private let performingInlineSearch = ValuePromise(false, ignoreRepeated: true) private var preloadHistoryPeerId: PeerId? private let preloadHistoryPeerIdDisposable = MetaDisposable() @@ -4543,7 +4544,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil })) } - }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) + }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get())) switch self.chatLocation { case let .peer(peerId): @@ -5127,13 +5128,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil }) } + if case .contextRequest = kind { + self.performingInlineSearch.set(false) + } case let .update(query, signal): let currentQueryAndDisposable = self.contextQueryStates[kind] currentQueryAndDisposable?.1.dispose() var inScope = true var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? - self.contextQueryStates[kind] = (query, (signal |> deliverOnMainQueue).start(next: { [weak self] result in + self.contextQueryStates[kind] = (query, (signal + |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { if Thread.isMainThread && inScope { inScope = false @@ -5148,13 +5153,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, error: { [weak self] error in if let strongSelf = self { + if case .contextRequest = kind { + strongSelf.performingInlineSearch.set(false) + } + switch error { - case let .inlineBotLocationRequest(peerId): - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: Int32(Date().timeIntervalSince1970 + 10 * 60)).start() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).start() - })]), in: .window(.root)) + case let .inlineBotLocationRequest(peerId): + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: Int32(Date().timeIntervalSince1970 + 10 * 60)).start() + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).start() + })]), in: .window(.root)) + } + } + }, completed: { [weak self] in + if let strongSelf = self { + if case .contextRequest = kind { + strongSelf.performingInlineSearch.set(false) } } })) @@ -5163,6 +5178,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in return inScopeResult(previousResult) }) + } else { + if case .contextRequest = kind { + self.performingInlineSearch.set(true) + } } if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index c7f0cf62fa..9c341337b9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1201,7 +1201,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if transition.isAnimated, let derivedLayoutState = self.derivedLayoutState { let offset = derivedLayoutState.inputContextPanelsOverMainPanelFrame.maxY - inputContextPanelsOverMainPanelFrame.maxY - transition.animateOffsetAdditive(node: self.inputContextPanelContainer, offset: -offset) + //transition.animateOffsetAdditive(node: self.inputContextPanelContainer, offset: -offset) } if let inputContextPanelNode = self.inputContextPanelNode { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index 470d350837..90ddfeb0cf 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -71,14 +71,14 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa switch inputQueryResult { case let .stickers(results): if !results.isEmpty { - if let currentPanel = currentPanel as? HorizontalStickersChatContextPanelNode { - currentPanel.updateResults(results.map({ $0.file })) + if let currentPanel = currentPanel as? InlineReactionSearchPanel { + currentPanel.updateResults(results: results.map({ $0.file })) return currentPanel } else { - let panel = HorizontalStickersChatContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) + let panel = InlineReactionSearchPanel(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) panel.controllerInteraction = controllerInteraction panel.interfaceInteraction = interfaceInteraction - panel.updateResults(results.map({ $0.file })) + panel.updateResults(results: results.map({ $0.file })) return panel } } @@ -94,7 +94,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa return panel } } - case let .emojis(results, range): + case let .emojis(results, _): if !results.isEmpty { if let currentPanel = currentPanel as? EmojisChatInputContextPanelNode { currentPanel.updateResults(results) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift index f7af875c4e..237101ed0f 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift @@ -26,13 +26,25 @@ private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) { final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private let account: Account + private var theme: PresentationTheme + private var strings: PresentationStrings private let controllerInteraction: ChatControllerInteraction private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void private let openGifContextMenu: (FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void - let searchPlaceholderNode: PaneSearchBarPlaceholderNode + private let searchPlaceholderNode: PaneSearchBarPlaceholderNode + var visibleSearchPlaceholderNode: PaneSearchBarPlaceholderNode? { + guard let scrollNode = multiplexedNode?.scrollNode else { + return nil + } + if scrollNode.bounds.contains(self.searchPlaceholderNode.frame) { + return self.searchPlaceholderNode + } + return nil + } + private var multiplexedNode: MultiplexedVideoNode? private let emptyNode: ImmediateTextNode @@ -46,6 +58,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void) { self.account = account + self.theme = theme + self.strings = strings self.controllerInteraction = controllerInteraction self.paneDidScroll = paneDidScroll self.fixPaneScroll = fixPaneScroll @@ -64,7 +78,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { self.addSubnode(self.emptyNode) self.searchPlaceholderNode.activate = { [weak self] in - self?.inputNodeInteraction?.toggleSearch(true, .gif) + self?.inputNodeInteraction?.toggleSearch(true, .gif, "") } self.updateThemeAndStrings(theme: theme, strings: strings) @@ -75,6 +89,9 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + self.emptyNode.attributedText = NSAttributedString(string: strings.Gif_NoGifsPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) self.searchPlaceholderNode.setup(theme: theme, strings: strings, type: .gifs) @@ -108,7 +125,11 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } override var isEmpty: Bool { - return self.multiplexedNode?.files.isEmpty ?? true + if let files = self.multiplexedNode?.files { + return files.trending.isEmpty && files.saved.isEmpty + } else { + return true + } } override func willEnterHierarchy() { @@ -118,7 +139,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } private func updateMultiplexedNodeLayout(changedIsExpanded: Bool, transition: ContainedViewLayoutTransition) { - guard let (size, topInset, bottomInset, isExpanded, isVisible, deviceMetrics) = self.validLayout else { + guard let (size, topInset, bottomInset, isExpanded, _, deviceMetrics) = self.validLayout else { return } @@ -137,52 +158,79 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size) if changedIsExpanded { - targetBounds.origin.y = isExpanded || multiplexedNode.files.isEmpty ? 0.0 : 60.0 + let isEmpty = multiplexedNode.files.trending.isEmpty && multiplexedNode.files.saved.isEmpty + //targetBounds.origin.y = isExpanded || isEmpty ? 0.0 : 60.0 } - transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: targetBounds) + //transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: targetBounds) transition.updateFrame(node: multiplexedNode, frame: nodeFrame) - multiplexedNode.updateLayout(size: nodeFrame.size, transition: transition) + multiplexedNode.updateLayout(theme: self.theme, strings: self.strings, size: nodeFrame.size, transition: transition) self.searchPlaceholderNode.frame = CGRect(x: 0.0, y: 41.0, width: size.width, height: 56.0) } } func initializeIfNeeded() { if self.multiplexedNode == nil { - self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", updateActivity: nil)) + self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, updateActivity: nil) + |> map { items -> [FileMediaReference]? in + if let (items, _) = items { + return items + } else { + return nil + } + }) - let multiplexedNode = MultiplexedVideoNode(account: account) + let multiplexedNode = MultiplexedVideoNode(account: self.account, theme: self.theme, strings: self.strings) self.multiplexedNode = multiplexedNode if let layout = self.validLayout { multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout.0) } + multiplexedNode.reactionSelected = { [weak self] reaction in + guard let strongSelf = self else { + return + } + strongSelf.inputNodeInteraction?.toggleSearch(true, .gif, reaction) + } + self.addSubnode(multiplexedNode) multiplexedNode.scrollNode.addSubnode(self.searchPlaceholderNode) - let gifs = self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]) - |> map { view -> [FileMediaReference] in + let gifs = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)])) + |> map { trending, view -> MultiplexedVideoNodeFiles in var recentGifs: OrderedItemListView? if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] { recentGifs = orderedView as? OrderedItemListView } + + var saved: [FileMediaReference] = [] + if let recentGifs = recentGifs { - return recentGifs.items.map { item in + saved = recentGifs.items.map { item in let file = (item.contents as! RecentMediaItem).media as! TelegramMediaFile return .savedGif(media: file) } } else { - return [] + saved = [] } + + return MultiplexedVideoNodeFiles(saved: saved, trending: trending ?? []) } self.disposable.set((gifs - |> deliverOnMainQueue).start(next: { [weak self] gifs in + |> deliverOnMainQueue).start(next: { [weak self] files in if let strongSelf = self { let previousFiles = strongSelf.multiplexedNode?.files - strongSelf.multiplexedNode?.files = gifs - strongSelf.emptyNode.isHidden = !gifs.isEmpty - if (previousFiles ?? []).isEmpty && !gifs.isEmpty { + strongSelf.multiplexedNode?.files = files + let wasEmpty: Bool + if let previousFiles = previousFiles { + wasEmpty = previousFiles.trending.isEmpty && previousFiles.saved.isEmpty + } else { + wasEmpty = true + } + let isEmpty = files.trending.isEmpty && files.saved.isEmpty + strongSelf.emptyNode.isHidden = !isEmpty + if wasEmpty && isEmpty { strongSelf.multiplexedNode?.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 60.0) } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift b/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift index 005f2649f5..a778e27499 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift @@ -165,7 +165,7 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable { switch self { case let .search(theme, strings): return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: { - inputNodeInteraction.toggleSearch(true, .sticker) + inputNodeInteraction.toggleSearch(true, .sticker, "") }) case let .peerSpecificSetup(theme, strings, dismissed): return StickerPanePeerSpecificSetupGridItem(theme: theme, strings: strings, setup: { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 6cf9c17d20..c3c340703d 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -332,7 +332,7 @@ private enum StickerPacksCollectionUpdate { final class ChatMediaInputNodeInteraction { let navigateToCollectionId: (ItemCollectionId) -> Void let openSettings: () -> Void - let toggleSearch: (Bool, ChatMediaInputSearchMode?) -> Void + let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void let openPeerSpecificSettings: () -> Void let dismissPeerSpecificSettings: () -> Void let clearRecentlyUsedStickers: () -> Void @@ -343,7 +343,7 @@ final class ChatMediaInputNodeInteraction { var previewedStickerPackItem: StickerPreviewPeekItem? var appearanceTransition: CGFloat = 1.0 - init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) { + init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) { self.navigateToCollectionId = navigateToCollectionId self.openSettings = openSettings self.toggleSearch = toggleSearch @@ -551,7 +551,7 @@ final class ChatMediaInputNode: ChatInputNode { controller.navigationPresentation = .modal strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) } - }, toggleSearch: { [weak self] value, searchMode in + }, toggleSearch: { [weak self] value, searchMode, query in if let strongSelf = self { if let searchMode = searchMode, value { var searchContainerNode: PaneSearchContainerNode? @@ -560,9 +560,14 @@ final class ChatMediaInputNode: ChatInputNode { } else { searchContainerNode = PaneSearchContainerNode(context: strongSelf.context, theme: strongSelf.theme, strings: strongSelf.strings, controllerInteraction: strongSelf.controllerInteraction, inputNodeInteraction: strongSelf.inputNodeInteraction, mode: searchMode, trendingGifsPromise: strongSelf.gifPane.trendingPromise, cancel: { self?.searchContainerNode?.deactivate() - self?.inputNodeInteraction.toggleSearch(false, nil) + self?.inputNodeInteraction.toggleSearch(false, nil, "") }) strongSelf.searchContainerNode = searchContainerNode + if !query.isEmpty { + DispatchQueue.main.async { + searchContainerNode?.updateQuery(query) + } + } } if let searchContainerNode = searchContainerNode { strongSelf.searchContainerNodeLoadedDisposable.set((searchContainerNode.ready @@ -1407,10 +1412,12 @@ final class ChatMediaInputNode: ChatInputNode { searchContainerNode.frame = containerFrame searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: .immediate) var placeholderNode: PaneSearchBarPlaceholderNode? + var anchorTop = CGPoint(x: 0.0, y: 0.0) + var anchorTopView: UIView = self.view if let searchMode = searchMode { switch searchMode { case .gif: - placeholderNode = self.gifPane.searchPlaceholderNode + placeholderNode = self.gifPane.visibleSearchPlaceholderNode case .sticker: self.stickerPane.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { @@ -1427,11 +1434,9 @@ final class ChatMediaInputNode: ChatInputNode { } } - if let placeholderNode = placeholderNode { - searchContainerNode.animateIn(from: placeholderNode, transition: transition, completion: { [weak self] in - self?.gifPane.removeFromSupernode() - }) - } + searchContainerNode.animateIn(from: placeholderNode, anchorTop: anchorTop, anhorTopView: anchorTopView, transition: transition, completion: { [weak self] in + self?.gifPane.removeFromSupernode() + }) } } } @@ -1589,8 +1594,8 @@ final class ChatMediaInputNode: ChatInputNode { if let searchMode = searchMode { switch searchMode { case .gif: - placeholderNode = self.gifPane.searchPlaceholderNode - paneIsEmpty = self.gifPane.isEmpty + placeholderNode = self.gifPane.visibleSearchPlaceholderNode + paneIsEmpty = placeholderNode != nil case .sticker: paneIsEmpty = true self.stickerPane.gridNode.forEachItemNode { itemNode in @@ -1612,11 +1617,14 @@ final class ChatMediaInputNode: ChatInputNode { } } if let placeholderNode = placeholderNode { + placeholderNode.isHidden = false searchContainerNode.animateOut(to: placeholderNode, animateOutSearchBar: !paneIsEmpty, transition: transition, completion: { [weak searchContainerNode] in searchContainerNode?.removeFromSupernode() }) } else { - searchContainerNode.removeFromSupernode() + transition.updateAlpha(node: searchContainerNode, alpha: 0.0, completion: { [weak searchContainerNode] _ in + searchContainerNode?.removeFromSupernode() + }) } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift index 4a3c664cf5..a3775c3877 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift @@ -338,7 +338,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { } }, getItemIsPreviewed: self.getItemIsPreviewed, openSearch: { [weak self] in - self?.inputNodeInteraction?.toggleSearch(true, .trending) + self?.inputNodeInteraction?.toggleSearch(true, .trending, "") }) let isPane = self.isPane diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 59903cb84a..504eb8c5fe 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -726,7 +726,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let mediaManager = context.sharedContext.mediaManager let streamVideo = isMediaStreamable(message: message, media: updatedVideoFile) - let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor) + let loopVideo = updatedVideoFile.isAnimated + let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor) let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in diff --git a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift index 4a5406524c..1932e40d73 100644 --- a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift @@ -21,13 +21,15 @@ final class ChatPanelInterfaceInteractionStatuses { let unblockingPeer: Signal let searching: Signal let loadingMessage: Signal + let inlineSearch: Signal - init(editingMessage: Signal, startingBot: Signal, unblockingPeer: Signal, searching: Signal, loadingMessage: Signal) { + init(editingMessage: Signal, startingBot: Signal, unblockingPeer: Signal, searching: Signal, loadingMessage: Signal, inlineSearch: Signal) { self.editingMessage = editingMessage self.startingBot = startingBot self.unblockingPeer = unblockingPeer self.searching = searching self.loadingMessage = loadingMessage + self.inlineSearch = inlineSearch } } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index c284cba3b6..4333403f2b 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import AsyncDisplayKit +import SwiftSignalKit import Postbox import TelegramCore import SyncCore @@ -11,18 +12,7 @@ import TextFormat import AccountContext import TouchDownGesture import ImageTransparency - -private let searchLayoutProgressImage = generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(rgb: 0x9099A2, alpha: 0.6).cgColor) - - let lineWidth: CGFloat = 2.0 - let cutoutWidth: CGFloat = 4.0 - context.setLineWidth(lineWidth) - - context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth))) - context.clear(CGRect(origin: CGPoint(x: (size.width - cutoutWidth) / 2.0, y: 0.0), size: CGSize(width: cutoutWidth, height: size.height / 2.0))) -}) +import ActivityIndicator private let accessoryButtonFont = Font.medium(14.0) @@ -217,7 +207,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let attachmentButton: HighlightableButtonNode let attachmentButtonDisabledNode: HighlightableButtonNode let searchLayoutClearButton: HighlightableButton - let searchLayoutProgressView: UIImageView + private let searchLayoutClearImageNode: ASImageNode + private var searchActivityIndicator: ActivityIndicator? var audioRecordingInfoContainerNode: ASDisplayNode? var audioRecordingDotNode: ASImageNode? var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode? @@ -281,6 +272,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + private let statusDisposable = MetaDisposable() + override var interfaceInteraction: ChatPanelInterfaceInteraction? { + didSet { + if let statuses = self.interfaceInteraction?.statuses { + self.statusDisposable.set((statuses.inlineSearch + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] value in + self?.updateIsProcessingInlineRequest(value) + })) + } + } + } + func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { if state.inputText.length != 0 && self.textInputNode == nil { self.loadTextInputNode() @@ -390,8 +394,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.attachmentButton.isAccessibilityElement = true self.attachmentButtonDisabledNode = HighlightableButtonNode() self.searchLayoutClearButton = HighlightableButton() - self.searchLayoutProgressView = UIImageView(image: searchLayoutProgressImage) - self.searchLayoutProgressView.isHidden = true + self.searchLayoutClearImageNode = ASImageNode() + self.searchLayoutClearImageNode.isUserInteractionEnabled = false + self.searchLayoutClearButton.addSubnode(self.searchLayoutClearImageNode) self.actionButtons = ChatTextInputActionButtonsNode(theme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings, presentController: presentController) @@ -466,8 +471,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.searchLayoutClearButton.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside) self.searchLayoutClearButton.alpha = 0.0 - self.searchLayoutClearButton.addSubview(self.searchLayoutProgressView) - self.addSubnode(self.textInputContainer) self.addSubnode(self.textInputBackgroundNode) @@ -495,6 +498,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { fatalError("init(coder:) has not been implemented") } + deinit { + self.statusDisposable.dispose() + } + func loadTextInputNodeIfNeeded() { if self.textInputNode == nil { self.loadTextInputNode() @@ -735,7 +742,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight) - self.searchLayoutClearButton.setImage(PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme), for: []) + self.searchLayoutClearImageNode.image = PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme) if let audioRecordingDotNode = self.audioRecordingDotNode { audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme) @@ -1102,9 +1109,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight) let textFieldInsets = self.textFieldInsets(metrics: metrics) transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize)) - - let searchProgressSize = self.searchLayoutProgressView.bounds.size - transition.updateFrame(layer: self.searchLayoutProgressView.layer, frame: CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - searchProgressSize.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - searchProgressSize.height) / 2.0)), size: searchProgressSize)) + if let image = self.searchLayoutClearImageNode.image { + self.searchLayoutClearImageNode.frame = CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - image.size.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - image.size.height) / 2.0)), size: image.size) + } let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top + audioRecordingItemsVerticalOffset, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) @@ -1423,6 +1430,26 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + func updateIsProcessingInlineRequest(_ value: Bool) { + if value { + if self.searchActivityIndicator == nil, let currentState = self.presentationInterfaceState { + let searchActivityIndicator = ActivityIndicator(type: .custom(currentState.theme.list.itemAccentColor, 22.0, 1.0, false)) + searchActivityIndicator.isUserInteractionEnabled = false + self.searchActivityIndicator = searchActivityIndicator + let indicatorSize = searchActivityIndicator.measure(CGSize(width: 100.0, height: 100.0)) + let size = self.searchLayoutClearButton.bounds.size + searchActivityIndicator.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0) + 1.0), size: indicatorSize) + self.searchLayoutClearImageNode.isHidden = true + self.searchLayoutClearButton.addSubnode(searchActivityIndicator) + searchActivityIndicator.layer.sublayerTransform = CATransform3DMakeScale(0.5, 0.5, 1.0) + } + } else if let searchActivityIndicator = self.searchActivityIndicator { + self.searchActivityIndicator = nil + self.searchLayoutClearImageNode.isHidden = false + searchActivityIndicator.removeFromSupernode() + } + } + @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { self.sendButtonPressed() diff --git a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift index 7362cd166a..1b8f59fd3d 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift @@ -210,7 +210,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { } private func dequeueTransition() { - if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first { + if let validLayout = self.validLayout, let (transition, _) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift index 8488254b76..a4b12e98f9 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift @@ -274,7 +274,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { }, openSettings: { }, - toggleSearch: { _, _ in + toggleSearch: { _, _, _ in }, openPeerSpecificSettings: { }, diff --git a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift index 1a6fb33dd4..2c31faac0d 100644 --- a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift @@ -11,7 +11,7 @@ import AccountContext import WebSearchUI import AppBundle -func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bool) -> Void)?) -> Signal<[FileMediaReference]?, NoError> { +func paneGifSearchForQuery(account: Account, query: String, offset: String?, updateActivity: ((Bool) -> Void)?) -> Signal<([FileMediaReference], String?)?, NoError> { let delayRequest = true let contextBot = account.postbox.transaction { transaction -> String in @@ -34,7 +34,7 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo } |> 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: account.peerId, limit: 15) + let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, offset: offset ?? "", limit: 50) |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in return { _ in return .contextRequestResult(user, results) @@ -66,25 +66,46 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo } } return contextBot - |> mapToSignal { result -> Signal<[FileMediaReference]?, NoError> in + |> mapToSignal { result -> Signal<([FileMediaReference], String?)?, 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 .externalReference(externalReference): var imageResource: TelegramMediaResource? + var thumbnailResource: TelegramMediaResource? + var thumbnailIsVideo: Bool = false var uniqueId: Int64? if let content = externalReference.content { imageResource = content.resource if let resource = content.resource as? WebFileReferenceMediaResource { uniqueId = Int64(HashFunctions.murMurHash32(resource.url)) } - } else if let thumbnail = externalReference.thumbnail { - imageResource = thumbnail.resource + } + if let thumbnail = externalReference.thumbnail { + thumbnailResource = thumbnail.resource + if thumbnail.mimeType.hasPrefix("video/") { + thumbnailIsVideo = true + } } - if externalReference.type == "gif", let thumbnailResource = imageResource, let content = externalReference.content, let dimensions = content.dimensions { - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) + if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions { + var previews: [TelegramMediaImageRepresentation] = [] + var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = [] + if let thumbnailResource = thumbnailResource { + if thumbnailIsVideo { + videoThumbnails.append(TelegramMediaFile.VideoThumbnail( + dimensions: dimensions, + resource: thumbnailResource + )) + } else { + previews.append(TelegramMediaImageRepresentation( + dimensions: dimensions, + resource: thumbnailResource + )) + } + } + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) references.append(FileMediaReference.standalone(media: file)) } case let .internalReference(internalReference): @@ -93,7 +114,7 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo } } } - return .single(references) + return .single((references, collection?.nextOffset)) } else { return .complete() } @@ -119,6 +140,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { private let notFoundNode: ASImageNode private let notFoundLabel: ImmediateTextNode + private var nextOffset: (String, String)? + private var isLoadingNextResults: Bool = false + private var validLayout: CGSize? private let trendingPromise: Promise<[FileMediaReference]?> @@ -131,6 +155,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { var deactivateSearchBar: (() -> Void)? var updateActivity: ((Bool) -> Void)? + var requestUpdateQuery: ((String) -> Void)? + + private var hasInitialText = false init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[FileMediaReference]?>) { self.context = context @@ -167,27 +194,82 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { } func updateText(_ text: String, languageCode: String?) { - let signal: Signal<[FileMediaReference]?, NoError> + self.hasInitialText = true + self.isLoadingNextResults = true + + let signal: Signal<([FileMediaReference], String?)?, NoError> if !text.isEmpty { - signal = paneGifSearchForQuery(account: self.context.account, query: text, updateActivity: self.updateActivity) + signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: "", updateActivity: self.updateActivity) self.updateActivity?(true) } else { signal = self.trendingPromise.get() + |> map { items -> ([FileMediaReference], String?)? in + if let items = items { + return (items, nil) + } else { + return nil + } + } self.updateActivity?(false) } self.searchDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self, let result = result else { + guard let strongSelf = self, let (result, nextOffset) = result else { return } - strongSelf.multiplexedNode?.files = result + strongSelf.isLoadingNextResults = false + if let nextOffset = nextOffset { + strongSelf.nextOffset = (text, nextOffset) + } else { + strongSelf.nextOffset = nil + } + strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: result) strongSelf.updateActivity?(false) strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty })) } + private func loadMore() { + if self.isLoadingNextResults { + return + } + guard let (text, nextOffsetValue) = self.nextOffset else { + return + } + self.isLoadingNextResults = true + + let signal: Signal<([FileMediaReference], String?)?, NoError> + signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity) + + self.searchDisposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self, let (result, nextOffset) = result else { + return + } + + var files = strongSelf.multiplexedNode?.files.trending ?? [] + var currentIds = Set(files.map { $0.media.fileId }) + for item in result { + if currentIds.contains(item.media.fileId) { + continue + } + currentIds.insert(item.media.fileId) + files.append(item) + } + + strongSelf.isLoadingNextResults = false + if let nextOffset = nextOffset { + strongSelf.nextOffset = (text, nextOffset) + } else { + strongSelf.nextOffset = nil + } + strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: files) + strongSelf.notFoundNode.isHidden = text.isEmpty || !files.isEmpty + })) + } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GifsNotFoundIcon"), color: theme.list.freeMonoIconColor) self.notFoundLabel.attributedText = NSAttributedString(string: strings.Gif_NoGifsFound, font: Font.medium(14.0), textColor: theme.list.freeTextColor) @@ -223,10 +305,10 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { 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) + multiplexedNode.updateLayout(theme: self.theme, strings: self.strings, size: nodeFrame.size, transition: transition) } - if firstLayout { + if firstLayout && !self.hasInitialText { self.updateText("", languageCode: nil) } } @@ -235,7 +317,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { super.willEnterHierarchy() if self.multiplexedNode == nil { - let multiplexedNode = MultiplexedVideoNode(account: self.context.account) + let multiplexedNode = MultiplexedVideoNode(account: self.context.account, theme: self.theme, strings: self.strings) self.multiplexedNode = multiplexedNode if let layout = self.validLayout { multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout) @@ -248,7 +330,19 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { } multiplexedNode.didScroll = { [weak self] offset, height in - self?.deactivateSearchBar?() + guard let strongSelf = self, let multiplexedNode = strongSelf.multiplexedNode else { + return + } + + strongSelf.deactivateSearchBar?() + + if offset >= height - multiplexedNode.bounds.height - 200.0 { + strongSelf.loadMore() + } + } + + multiplexedNode.reactionSelected = { [weak self] reaction in + self?.requestUpdateQuery?(reaction) } } } diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index 16763c248f..150e806eba 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -281,7 +281,10 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont if let (transition, firstTime) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) - let options = ListViewDeleteAndInsertOptions() + var options = ListViewDeleteAndInsertOptions() + options.insert(.Synchronous) + options.insert(.LowLatency) + options.insert(.PreferSynchronousResourceLoading) if firstTime { //options.insert(.Synchronous) //options.insert(.LowLatency) diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index da1f53a415..edefa1d538 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -40,7 +40,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { Queue.mainQueue().async { completion(node, { - return (nil, { _ in apply(.None) }) + return (nil, { _ in apply(synchronousLoads, .None) }) }) } } @@ -64,7 +64,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { let (layout, apply) = nodeLayout(self, params, top, bottom) Queue.mainQueue().async { completion(layout, { _ in - apply(animation) + apply(false, animation) }) } } @@ -188,11 +188,11 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) self.contentSize = layout.contentSize self.insets = layout.insets - apply(.None) + apply(false, .None) } } - func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (Bool, ListViewItemUpdateAnimation) -> Void) { let imageLayout = self.imageNode.asyncLayout() let currentImageResource = self.currentImageResource let currentVideoFile = self.currentVideoFile @@ -315,7 +315,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } else { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0)), resource: imageResource) let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) - updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage)) + updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage), synchronousLoad: true) } } else { updateImageSignal = .complete() @@ -324,7 +324,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: height, height: croppedImageDimensions.width + sideInset), insets: UIEdgeInsets()) - return (nodeLayout, { _ in + return (nodeLayout, { synchronousLoads, _ in if let strongSelf = self { strongSelf.item = item strongSelf.currentImageResource = imageResource @@ -333,7 +333,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode if let imageApply = imageApply { if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(updateImageSignal) + strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: true) } strongSelf.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) @@ -351,7 +351,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } if let videoFile = videoFile { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, fileReference: .standalone(media: videoFile)) + let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads) thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) strongSelf.layer.addSublayer(thumbnailLayer) let layerHolder = takeSampleBufferLayer() diff --git a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift index 0b08d86d18..2f50ddb6de 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift @@ -14,22 +14,22 @@ import TelegramAnimatedStickerNode final class HorizontalStickerGridItem: GridItem { let account: Account let file: TelegramMediaFile - let stickersInteraction: HorizontalStickersChatContextPanelInteraction - let interfaceInteraction: ChatPanelInterfaceInteraction + let isPreviewed: (HorizontalStickerGridItem) -> Bool + let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Void let section: GridSection? = nil - init(account: Account, file: TelegramMediaFile, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) { + init(account: Account, file: TelegramMediaFile, isPreviewed: @escaping (HorizontalStickerGridItem) -> Bool, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Void) { self.account = account self.file = file - self.stickersInteraction = stickersInteraction - self.interfaceInteraction = interfaceInteraction + self.isPreviewed = isPreviewed + self.sendSticker = sendSticker } func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = HorizontalStickerGridItemNode() node.setup(account: self.account, item: self) - node.interfaceInteraction = self.interfaceInteraction + node.sendSticker = self.sendSticker return node } @@ -39,7 +39,7 @@ final class HorizontalStickerGridItem: GridItem { return } node.setup(account: self.account, item: self) - node.interfaceInteraction = self.interfaceInteraction + node.sendSticker = self.sendSticker } } @@ -50,7 +50,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { private let stickerFetchedDisposable = MetaDisposable() - var interfaceInteraction: ChatPanelInterfaceInteraction? + var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)? private var currentIsPreviewing: Bool = false @@ -108,11 +108,14 @@ final class HorizontalStickerGridItemNode: GridItemNode { self.addSubnode(animationNode) self.animationNode = animationNode } + + let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) + + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: item.file, small: true, size: fittedDimensions, synchronousLoad: false)) animationNode.started = { [weak self] in self?.imageNode.alpha = 0.0 } - let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start()) @@ -158,8 +161,8 @@ final class HorizontalStickerGridItemNode: GridItemNode { } @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { - if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { - let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), self, self.bounds) + if let (_, item, _) = self.currentState, case .ended = recognizer.state { + self.sendSticker?(.standalone(media: item.file), self, self.bounds) } } @@ -170,7 +173,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { func updatePreviewing(animated: Bool) { var isPreviewing = false if let (_, item, _) = self.currentState { - isPreviewing = item.stickersInteraction.previewedStickerItem == self.stickerItem + //isPreviewing = item.isPreviewed(self.stickerItem) } if self.currentIsPreviewing != isPreviewing { self.currentIsPreviewing = isPreviewing diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 6cfd6880a6..d3fc4c399d 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -66,7 +66,11 @@ private struct StickerEntry: Identifiable, Comparable { } func item(account: Account, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> GridItem { - return HorizontalStickerGridItem(account: account, file: self.file, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction) + return HorizontalStickerGridItem(account: account, file: self.file, isPreviewed: { item in + return false//stickersInteraction.previewedStickerItem == item + }, sendSticker: { file, node, rect in + let _ = interfaceInteraction.sendSticker(file, node, rect) + }) } } diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift new file mode 100644 index 0000000000..11dc1e29d6 --- /dev/null +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -0,0 +1,388 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import AsyncDisplayKit +import TelegramCore +import SyncCore +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext + +private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollViewDelegate { + private final class DisplayItem { + let file: TelegramMediaFile + let frame: CGRect + + init(file: TelegramMediaFile, frame: CGRect) { + self.file = file + self.frame = frame + } + } + + private let context: AccountContext + + private let scrollNode: ASScrollNode + private var items: [TelegramMediaFile] = [] + private var displayItems: [DisplayItem] = [] + private var topInset: CGFloat? + private var itemNodes: [MediaId: HorizontalStickerGridItemNode] = [:] + + private var validLayout: CGSize? + private var ignoreScrolling: Bool = false + private var animateInOnLayout: Bool = false + + var updateBackgroundOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)? + + init(context: AccountContext) { + self.context = context + + self.scrollNode = ASScrollNode() + + super.init() + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.delegate = self + + self.addSubnode(self.scrollNode) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateVisibleItems(synchronous: false) + self.updateBackground(transition: .immediate) + } + } + + private func updateBackground(transition: ContainedViewLayoutTransition) { + if let topInset = self.topInset { + self.updateBackgroundOffset?(max(0.0, -self.scrollNode.view.contentOffset.y + topInset), transition) + } + } + + func updateScrollNode() { + guard let size = self.validLayout else { + return + } + var contentHeight: CGFloat = 0.0 + if let item = self.displayItems.last { + let maxY = item.frame.maxY + 4.0 + + var topInset = size.height - floor(item.frame.height * 1.5) + if topInset + maxY < size.height { + topInset = size.height - maxY + } + self.topInset = topInset + contentHeight = topInset + maxY + } else { + self.topInset = size.height + } + self.scrollNode.view.contentSize = CGSize(width: size.width, height: max(contentHeight, size.height)) + } + + func updateItems(items: [TelegramMediaFile]) { + self.items = items + + var previousBackgroundOffset: CGFloat? + if let topInset = self.topInset { + previousBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset) + } else { + previousBackgroundOffset = self.validLayout?.height + } + + if let size = self.validLayout { + self.updateItemsLayout(width: size.width) + self.updateScrollNode() + } + + self.updateVisibleItems(synchronous: true) + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring) + + if let previousBackgroundOffset = previousBackgroundOffset, let topInset = self.topInset { + let currentBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset) + if abs(currentBackgroundOffset - previousBackgroundOffset) > .ulpOfOne { + transition.animateOffsetAdditive(node: self.scrollNode, offset: currentBackgroundOffset - previousBackgroundOffset) + self.updateBackground(transition: transition) + } + } else { + self.animateInOnLayout = true + } + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + var previousBackgroundOffset: CGFloat? + if let topInset = self.topInset { + previousBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset) + } else { + previousBackgroundOffset = self.validLayout?.height + } + + let previousLayout = self.validLayout + self.validLayout = size + + if self.animateInOnLayout { + self.updateBackgroundOffset?(size.height, .immediate) + } + + var synchronous = false + if previousLayout?.width != size.width { + synchronous = true + self.updateItemsLayout(width: size.width) + } + + self.ignoreScrolling = true + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) + self.updateScrollNode() + self.ignoreScrolling = false + + self.updateVisibleItems(synchronous: synchronous) + + var backgroundTransition = transition + + if self.animateInOnLayout { + self.animateInOnLayout = false + backgroundTransition = .animated(duration: 0.3, curve: .spring) + if let topInset = self.topInset { + let currentBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset) + backgroundTransition.animateOffsetAdditive(node: self.scrollNode, offset: currentBackgroundOffset - size.height) + } + } else { + if let previousBackgroundOffset = previousBackgroundOffset, let topInset = self.topInset { + let currentBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset) + if abs(currentBackgroundOffset - previousBackgroundOffset) > .ulpOfOne { + transition.animateOffsetAdditive(node: self.scrollNode, offset: currentBackgroundOffset - previousBackgroundOffset) + } + } + } + + self.updateBackground(transition: backgroundTransition) + } + + private func updateItemsLayout(width: CGFloat) { + self.displayItems.removeAll() + + let itemsPerRow = min(8, max(4, Int(width / 80))) + let sideInset: CGFloat = 4.0 + let itemSpacing: CGFloat = 4.0 + let itemSize = floor((width - sideInset * 2.0 - itemSpacing * (CGFloat(itemsPerRow) - 1.0)) / CGFloat(itemsPerRow)) + + var columnIndex = 0 + var topOffset: CGFloat = 7.0 + for i in 0 ..< self.items.count { + self.displayItems.append(DisplayItem(file: self.items[i], frame: CGRect(origin: CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: topOffset), size: CGSize(width: itemSize, height: itemSize)))) + + columnIndex += 1 + if columnIndex == itemsPerRow { + columnIndex = 0 + topOffset += itemSize + } + } + } + + private func updateVisibleItems(synchronous: Bool) { + guard let _ = self.validLayout, let topInset = self.topInset else { + return + } + + var minVisibleY = self.scrollNode.view.bounds.minY + var maxVisibleY = self.scrollNode.view.bounds.maxY + + let minActivatedY = minVisibleY + let maxActivatedY = maxVisibleY + + minVisibleY -= 200.0 + maxVisibleY += 200.0 + + var validIds = Set() + for i in 0 ..< self.displayItems.count { + let item = self.displayItems[i] + + let itemFrame = item.frame.offsetBy(dx: 0.0, dy: topInset) + + if itemFrame.maxY >= minVisibleY { + let isActivated = itemFrame.maxY >= minActivatedY && itemFrame.minY <= maxActivatedY + + let itemNode: HorizontalStickerGridItemNode + if let current = self.itemNodes[item.file.fileId] { + itemNode = current + } else { + let item = HorizontalStickerGridItem( + account: self.context.account, + file: item.file, + isPreviewed: { _ in + return false + }, sendSticker: { [weak self] file, node, rect in + self?.sendSticker?(file, node, rect) + } + ) + itemNode = item.node(layout: GridNodeLayout( + size: CGSize(), + insets: UIEdgeInsets(), + scrollIndicatorInsets: nil, + preloadSize: 0.0, + type: .fixed(itemSize: CGSize(), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil) + ), synchronousLoad: synchronous) as! HorizontalStickerGridItemNode + itemNode.subnodeTransform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + self.itemNodes[item.file.fileId] = itemNode + self.scrollNode.addSubnode(itemNode) + } + itemNode.frame = itemFrame + itemNode.isVisibleInGrid = isActivated + validIds.insert(item.file.fileId) + } + if itemFrame.minY > maxVisibleY { + break + } + } + + var removeIds: [MediaId] = [] + for (id, itemNode) in self.itemNodes { + if !validIds.contains(id) { + removeIds.append(id) + itemNode.removeFromSupernode() + } + } + for id in removeIds { + self.itemNodes.removeValue(forKey: id) + } + } +} + +private let backroundDiameter: CGFloat = 20.0 +private let shadowBlur: CGFloat = 6.0 + +final class InlineReactionSearchPanel: ChatInputContextPanelNode { + private let containerNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let backgroundTopLeftNode: ASImageNode + private let backgroundTopLeftContainerNode: ASDisplayNode + private let backgroundTopRightNode: ASImageNode + private let backgroundTopRightContainerNode: ASDisplayNode + private let backgroundContainerNode: ASDisplayNode + private let stickersNode: InlineReactionSearchStickersNode + + var controllerInteraction: ChatControllerInteraction? + + private var validLayout: (CGSize, CGFloat)? + + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { + self.containerNode = ASDisplayNode() + + self.backgroundNode = ASDisplayNode() + + let shadowImage = generateImage(CGSize(width: backroundDiameter + shadowBlur * 2.0, height: floor(backroundDiameter / 2.0 + shadowBlur)), rotatedContext: { size, context in + let diameter = backroundDiameter + let shadow = UIColor(white: 0.0, alpha: 0.5) + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.saveGState() + context.setFillColor(shadow.cgColor) + context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + + context.restoreGState() + + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(backroundDiameter / 2.0 + shadowBlur), topCapHeight: 0) + + self.backgroundTopLeftNode = ASImageNode() + self.backgroundTopLeftNode.image = shadowImage + self.backgroundTopLeftContainerNode = ASDisplayNode() + self.backgroundTopLeftContainerNode.clipsToBounds = true + self.backgroundTopLeftContainerNode.addSubnode(self.backgroundTopLeftNode) + + self.backgroundTopRightNode = ASImageNode() + self.backgroundTopRightNode.image = shadowImage + self.backgroundTopRightContainerNode = ASDisplayNode() + self.backgroundTopRightContainerNode.clipsToBounds = true + self.backgroundTopRightContainerNode.addSubnode(self.backgroundTopRightNode) + + self.backgroundContainerNode = ASDisplayNode() + + self.stickersNode = InlineReactionSearchStickersNode(context: context) + + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) + + self.placement = .overPanels + self.isOpaque = false + self.clipsToBounds = true + + self.backgroundContainerNode.addSubnode(self.backgroundNode) + self.backgroundContainerNode.addSubnode(self.backgroundTopLeftContainerNode) + self.backgroundContainerNode.addSubnode(self.backgroundTopRightContainerNode) + self.containerNode.addSubnode(self.backgroundContainerNode) + self.containerNode.addSubnode(self.stickersNode) + + self.addSubnode(self.containerNode) + + self.backgroundNode.backgroundColor = .white + + self.stickersNode.updateBackgroundOffset = { [weak self] offset, transition in + guard let strongSelf = self, let (_, _) = strongSelf.validLayout else { + return + } + transition.updateFrame(node: strongSelf.backgroundContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize()), beginWithCurrentState: false) + + let cornersTransitionDistance: CGFloat = 20.0 + let cornersTransition: CGFloat = max(0.0, min(1.0, (cornersTransitionDistance - offset) / cornersTransitionDistance)) + transition.updateSublayerTransformScaleAndOffset(node: strongSelf.backgroundTopLeftContainerNode, scale: 1.0, offset: CGPoint(x: -cornersTransition * backroundDiameter, y: 0.0), beginWithCurrentState: true) + transition.updateSublayerTransformScaleAndOffset(node: strongSelf.backgroundTopRightContainerNode, scale: 1.0, offset: CGPoint(x: cornersTransition * backroundDiameter, y: 0.0), beginWithCurrentState: true) + } + + self.stickersNode.sendSticker = { [weak self] file, node, rect in + guard let strongSelf = self else { + return + } + let _ = strongSelf.controllerInteraction?.sendSticker(file, true, node, rect) + } + } + + func updateResults(results: [TelegramMediaFile]) { + self.stickersNode.updateItems(items: results) + } + + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + self.validLayout = (size, leftInset) + + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: backroundDiameter / 2.0), size: size)) + + transition.updateFrame(node: self.backgroundTopLeftContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -shadowBlur), size: CGSize(width: size.width / 2.0, height: backroundDiameter / 2.0 + shadowBlur))) + transition.updateFrame(node: self.backgroundTopRightContainerNode, frame: CGRect(origin: CGPoint(x: size.width / 2.0, y: -shadowBlur), size: CGSize(width: size.width - size.width / 2.0, height: backroundDiameter / 2.0 + shadowBlur))) + + transition.updateFrame(node: self.backgroundTopLeftNode, frame: CGRect(origin: CGPoint(x: -shadowBlur, y: 0.0), size: CGSize(width: size.width + shadowBlur * 2.0, height: backroundDiameter / 2.0 + shadowBlur))) + transition.updateFrame(node: self.backgroundTopRightNode, frame: CGRect(origin: CGPoint(x: -shadowBlur - size.width / 2.0, y: 0.0), size: CGSize(width: size.width + shadowBlur * 2.0, height: backroundDiameter / 2.0 + shadowBlur))) + + transition.updateFrame(node: self.stickersNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: size.width - leftInset * 2.0, height: size.height))) + self.stickersNode.update(size: CGSize(width: size.width - leftInset * 2.0, height: size.height), transition: transition) + } + + override func animateOut(completion: @escaping () -> Void) { + self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.containerNode.bounds.height - self.backgroundContainerNode.frame.minY), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.backgroundNode.frame.contains(self.view.convert(point, to: self.backgroundNode.view)) { + return nil + } + return super.hitTest(point, with: event) + } +} diff --git a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift index ab7981295f..38d0e18e11 100644 --- a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift +++ b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift @@ -8,6 +8,7 @@ import TelegramCore import SyncCore import AVFoundation import ContextUI +import TelegramPresentationData private final class MultiplexedVideoTrackingNode: ASDisplayNode { var inHierarchyUpdated: ((Bool) -> Void)? @@ -26,20 +27,129 @@ private final class MultiplexedVideoTrackingNode: ASDisplayNode { } private final class VisibleVideoItem { + enum Id: Equatable, Hashable { + case saved(MediaId) + case trending(MediaId) + } + let id: Id let fileReference: FileMediaReference let frame: CGRect - init(fileReference: FileMediaReference, frame: CGRect) { + init(fileReference: FileMediaReference, frame: CGRect, isTrending: Bool) { self.fileReference = fileReference self.frame = frame + if isTrending { + self.id = .trending(fileReference.media.fileId) + } else { + self.id = .saved(fileReference.media.fileId) + } + } +} + +final class MultiplexedVideoNodeFiles { + let saved: [FileMediaReference] + let trending: [FileMediaReference] + + init(saved: [FileMediaReference], trending: [FileMediaReference]) { + self.saved = saved + self.trending = trending + } +} + +private final class TrendingHeaderNode: ASDisplayNode { + private let titleNode: ImmediateTextNode + private let reactions: [String] + private let reactionNodes: [ImmediateTextNode] + private let scrollNode: ASScrollNode + + var reactionSelected: ((String) -> Void)? + + override init() { + self.titleNode = ImmediateTextNode() + self.reactions = [ + "👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒" + ] + self.scrollNode = ASScrollNode() + let scrollNode = self.scrollNode + self.reactionNodes = reactions.map { reaction -> ImmediateTextNode in + let textNode = ImmediateTextNode() + textNode.attributedText = NSAttributedString(string: reaction, font: Font.regular(30.0), textColor: .black) + scrollNode.addSubnode(textNode) + return textNode + } + + super.init() + + self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.canCancelContentTouches = true + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.addSubnode(self.titleNode) + self.addSubnode(self.scrollNode) + + for i in 0 ..< self.reactionNodes.count { + self.reactionNodes[i].view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let location = recognizer.location(in: self.scrollNode.view) + for i in 0 ..< self.reactionNodes.count { + if self.reactionNodes[i].frame.contains(location) { + let reaction = self.reactions[i] + self.reactionSelected?(reaction) + break + } + } + } + } + + func update(theme: PresentationTheme, strings: PresentationStrings, width: CGFloat, sideInset: CGFloat) -> CGFloat { + let height: CGFloat = 72.0 + let leftInset: CGFloat = 10.0 + + //TODO:localize + self.titleNode.attributedText = NSAttributedString(string: "TRENDING GIFS", font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) + let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset * 2.0 - sideInset * 2.0, height: 100.0)) + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: titleSize) + + let reactionSizes = self.reactionNodes.map { reactionNode -> CGSize in + return reactionNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + } + + let reactionSpacing: CGFloat = 4.0 + var reactionsOffset: CGFloat = leftInset - 2.0 + + for i in 0 ..< self.reactionNodes.count { + if i != 0 { + reactionsOffset += reactionSpacing + } + reactionNodes[i].frame = CGRect(origin: CGPoint(x: reactionsOffset, y: 0.0), size: reactionSizes[i]) + reactionsOffset += reactionSizes[i].width + } + reactionsOffset += leftInset - 2.0 + + self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 28.0), size: CGSize(width: width, height: 44.0)) + self.scrollNode.view.contentSize = CGSize(width: reactionsOffset, height: 44.0) + + return height } } final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { private let account: Account + private var theme: PresentationTheme + private var strings: PresentationStrings private let trackingNode: MultiplexedVideoTrackingNode var didScroll: ((CGFloat, CGFloat) -> Void)? var didEndScrolling: (() -> Void)? + var reactionSelected: ((String) -> Void)? var topInset: CGFloat = 0.0 { didSet { @@ -59,21 +169,24 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } } - var files: [FileMediaReference] = [] { + var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: []) { didSet { let startTime = CFAbsoluteTimeGetCurrent() - self.updateVisibleItems() + self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: true) print("MultiplexedVideoNode files updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") } } private var displayItems: [VisibleVideoItem] = [] - private var visibleThumbnailLayers: [MediaId: SoftwareVideoThumbnailLayer] = [:] - private var statusDisposable: [MediaId : MetaDisposable] = [:] + private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:] + private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:] private let contextContainerNode: ContextControllerSourceNode let scrollNode: ASScrollNode - private var visibleLayers: [MediaId: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:] + private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:] + + private let savedTitleNode: ImmediateTextNode + private let trendingHeaderNode: TrendingHeaderNode private var displayLink: CADisplayLink! private var timeOffset = 0.0 @@ -85,8 +198,10 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { var fileContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void)? var enableVideoNodes = false - init(account: Account) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.account = account + self.theme = theme + self.strings = strings self.trackingNode = MultiplexedVideoTrackingNode() self.trackingNode.isLayerBacked = true @@ -98,13 +213,26 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { self.contextContainerNode = ContextControllerSourceNode() self.scrollNode = ASScrollNode() + //TODO:localization + self.savedTitleNode = ImmediateTextNode() + self.savedTitleNode.attributedText = NSAttributedString(string: "MY GIFS", font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) + + self.trendingHeaderNode = TrendingHeaderNode() + super.init() + self.trendingHeaderNode.reactionSelected = { [weak self] reaction in + self?.reactionSelected?(reaction) + } + self.isOpaque = true self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.addSubnode(self.savedTitleNode) + self.scrollNode.addSubnode(self.trendingHeaderNode) + self.addSubnode(self.trackingNode) self.addSubnode(self.contextContainerNode) self.contextContainerNode.addSubnode(self.scrollNode) @@ -216,13 +344,16 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } private var validSize: CGSize? - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) { + self.theme = theme + self.strings = strings if self.validSize == nil || !self.validSize!.equalTo(size) { + let previousSize = self.validSize ?? CGSize() self.validSize = size self.contextContainerNode.frame = CGRect(origin: CGPoint(), size: size) - self.scrollNode.frame = CGRect(origin: CGPoint(), size: size) + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) let startTime = CFAbsoluteTimeGetCurrent() - self.updateVisibleItems(transition: transition) + self.updateVisibleItems(extendSizeForTransition: max(0.0, previousSize.height - size.height), transition: transition) print("MultiplexedVideoNode layout updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") } } @@ -242,9 +373,12 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } } + private var currentExtendSizeForTransition: CGFloat = 0.0 + private var validVisibleItemsOffset: CGFloat? - private func updateImmediatelyVisibleItems(ensureFrames: Bool = false) { - let visibleBounds = self.scrollNode.bounds + private func updateImmediatelyVisibleItems(ensureFrames: Bool = false, synchronous: Bool = false) { + var visibleBounds = self.scrollNode.bounds + visibleBounds.size.height += max(0.0, self.currentExtendSizeForTransition) let visibleThumbnailBounds = visibleBounds.insetBy(dx: 0.0, dy: -350.0) if let validVisibleItemsOffset = self.validVisibleItemsOffset, validVisibleItemsOffset.isEqual(to: visibleBounds.origin.y) { @@ -257,8 +391,8 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { let minVisibleThumbnailY = visibleThumbnailBounds.minY let maxVisibleThumbnailY = visibleThumbnailBounds.maxY - var visibleThumbnailIds = Set() - var visibleIds = Set() + var visibleThumbnailIds = Set() + var visibleIds = Set() for item in self.displayItems { if item.frame.maxY < minVisibleThumbnailY { @@ -268,17 +402,17 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { break; } - visibleThumbnailIds.insert(item.fileReference.media.fileId) + visibleThumbnailIds.insert(item.id) - if let thumbnailLayer = self.visibleThumbnailLayers[item.fileReference.media.fileId] { + if let thumbnailLayer = self.visibleThumbnailLayers[item.id] { if ensureFrames { thumbnailLayer.frame = item.frame } } else { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.fileReference) + let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.fileReference, synchronousLoad: synchronous) thumbnailLayer.frame = item.frame self.scrollNode.layer.addSublayer(thumbnailLayer) - self.visibleThumbnailLayers[item.fileReference.media.fileId] = thumbnailLayer + self.visibleThumbnailLayers[item.id] = thumbnailLayer } let progressSize = CGSize(width: 24.0, height: 24.0) @@ -291,9 +425,9 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { continue } - visibleIds.insert(item.fileReference.media.fileId) + visibleIds.insert(item.id) - if let (_, layerHolder) = self.visibleLayers[item.fileReference.media.fileId] { + if let (_, layerHolder) = self.visibleLayers[item.id] { if ensureFrames { layerHolder.layer.frame = item.frame } @@ -303,23 +437,23 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { layerHolder.layer.frame = item.frame self.scrollNode.layer.addSublayer(layerHolder.layer) let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.fileReference, layerHolder: layerHolder) - self.visibleLayers[item.fileReference.media.fileId] = (manager, layerHolder) - self.visibleThumbnailLayers[item.fileReference.media.fileId]?.ready = { [weak self] in + self.visibleLayers[item.id] = (manager, layerHolder) + self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in if let strongSelf = self { - strongSelf.visibleLayers[item.fileReference.media.fileId]?.0.start() + strongSelf.visibleLayers[item.id]?.0.start() } } } } - var removeIds: [MediaId] = [] + var removeIds: [VisibleVideoItem.Id] = [] for id in self.visibleLayers.keys { if !visibleIds.contains(id) { removeIds.append(id) } } - var removeThumbnailIds: [MediaId] = [] + var removeThumbnailIds: [VisibleVideoItem.Id] = [] for id in self.visibleThumbnailLayers.keys { if !visibleThumbnailIds.contains(id) { removeThumbnailIds.append(id) @@ -353,116 +487,255 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { }*/ } - private func updateVisibleItems(transition: ContainedViewLayoutTransition = .immediate) { + private func updateVisibleItems(extendSizeForTransition: CGFloat, transition: ContainedViewLayoutTransition, synchronous: Bool = false) { let drawableSize = self.scrollNode.bounds.size if !drawableSize.width.isZero { var displayItems: [VisibleVideoItem] = [] let idealHeight = self.idealHeight - var weights: [Int] = [] - var totalItemSize: CGFloat = 0.0 - for item in self.files { - let aspectRatio: CGFloat - if let dimensions = item.media.dimensions { - aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height - } else { - aspectRatio = 1.0 + var verticalOffset: CGFloat = self.topInset + + func commitFilesSpans(files: [FileMediaReference], isTrending: Bool) { + var rowsCount = 0 + var firstRowMax = 0; + + let viewPortAvailableSize = drawableSize.width + + let preferredRowSize: CGFloat = 100.0 + let itemsCount = files.count + let spanCount: CGFloat = 100.0 + var spanLeft = spanCount + var currentItemsInRow = 0 + var currentItemsSpanAmount: CGFloat = 0.0 + + var itemSpans: [Int: CGFloat] = [:] + var itemsToRow: [Int: Int] = [:] + + for a in 0 ..< itemsCount { + var size: CGSize + if let dimensions = files[a].media.dimensions { + size = dimensions.cgSize + } else { + size = CGSize(width: 100.0, height: 100.0) + } + if size.width <= 0.0 { + size.width = 100.0 + } + if size.height <= 0.0 { + size.height = 100.0 + } + //size = CGSize(width: 100.0, height: 100.0) + let aspect: CGFloat = size.width / size.height + if aspect > 4.0 || aspect < 0.2 { + size.width = max(size.width, size.height) + size.height = size.width + } + + var requiredSpan = min(spanCount, floor(spanCount * (size.width / size.height * preferredRowSize / viewPortAvailableSize))) + let moveToNewRow = spanLeft < requiredSpan || requiredSpan > 33.0 && spanLeft < requiredSpan - 15.0 + if moveToNewRow { + if spanLeft > 0 { + let spanPerItem = floor(spanLeft / CGFloat(currentItemsInRow)) + + let start = a - currentItemsInRow + var b = start + while b < start + currentItemsInRow { + if (b == start + currentItemsInRow - 1) { + itemSpans[b] = itemSpans[b]! + spanLeft + } else { + itemSpans[b] = itemSpans[b]! + spanPerItem + } + spanLeft -= spanPerItem; + + b += 1 + } + + itemsToRow[a - 1] = rowsCount + } + rowsCount += 1 + currentItemsSpanAmount = 0 + currentItemsInRow = 0 + spanLeft = spanCount + } else { + if spanLeft < requiredSpan { + requiredSpan = spanLeft + } + } + if rowsCount == 0 { + firstRowMax = max(firstRowMax, a) + } + if a == itemsCount - 1 { + itemsToRow[a] = rowsCount + } + currentItemsSpanAmount += requiredSpan + currentItemsInRow += 1 + spanLeft -= requiredSpan + spanLeft = max(0, spanLeft) + + itemSpans[a] = requiredSpan + } + if itemsCount != 0 { + rowsCount += 1 + } + + var currentRowHorizontalOffset: CGFloat = 0.0 + for index in 0 ..< files.count { + guard let width = itemSpans[index] else { + continue + } + let itemWidth = floor(width * drawableSize.width / 100.0) - 1 + + var itemSize = CGSize(width: itemWidth, height: preferredRowSize) + if itemsToRow[index] != nil { + itemSize.width = max(itemSize.width, drawableSize.width - currentRowHorizontalOffset) + } + displayItems.append(VisibleVideoItem(fileReference: files[index], frame: CGRect(origin: CGPoint(x: currentRowHorizontalOffset, y: verticalOffset), size: itemSize), isTrending: isTrending)) + currentRowHorizontalOffset += itemSize.width + 1.0 + + if itemsToRow[index] != nil { + verticalOffset += preferredRowSize + 1.0 + currentRowHorizontalOffset = 0.0 + } } - weights.append(Int(aspectRatio * 100)) - totalItemSize += aspectRatio * idealHeight } - let numberOfRows = max(Int(round(totalItemSize / drawableSize.width)), 1) - - let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows) - - var i = 0 - var offset = CGPoint(x: 0.0, y: self.topInset) - var previousItemSize: CGFloat = 0.0 - var contentMaxValueInScrollDirection: CGFloat = self.topInset - let maxWidth = drawableSize.width - - let minimumInteritemSpacing: CGFloat = 1.0 - let minimumLineSpacing: CGFloat = 1.0 - - let viewportWidth: CGFloat = drawableSize.width - - let preferredRowSize = idealHeight - - var rowIndex = -1 - for row in partition { - rowIndex += 1 - - var summedRatios: CGFloat = 0.0 - - var j = i - var n = i + row.count - - while j < n { + func commitFiles(files: [FileMediaReference], isTrending: Bool) { + var weights: [Int] = [] + var totalItemSize: CGFloat = 0.0 + for item in files { let aspectRatio: CGFloat - if let dimensions = self.files[j].media.dimensions { + if let dimensions = item.media.dimensions { aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height } else { aspectRatio = 1.0 } - - summedRatios += aspectRatio - - j += 1 + weights.append(Int(aspectRatio * 100)) + totalItemSize += aspectRatio * idealHeight } - var rowSize = drawableSize.width - (CGFloat(row.count - 1) * minimumInteritemSpacing) + let numberOfRows = max(Int(round(totalItemSize / drawableSize.width)), 1) - if rowIndex == partition.count - 1 { - if row.count < 2 { - rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing) - } else if row.count < 3 { - rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing) - } - } + let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows) - j = i - n = i + row.count + var i = 0 + var offset = CGPoint(x: 0.0, y: verticalOffset) + var previousItemSize: CGFloat = 0.0 + let maxWidth = drawableSize.width - while j < n { - let aspectRatio: CGFloat - if let dimensions = self.files[j].media.dimensions { - aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height - } else { - aspectRatio = 1.0 - } - let preferredAspectRatio = aspectRatio + let minimumInteritemSpacing: CGFloat = 1.0 + let minimumLineSpacing: CGFloat = 1.0 + + let viewportWidth: CGFloat = drawableSize.width + + let preferredRowSize = idealHeight + + var rowIndex = -1 + for row in partition { + rowIndex += 1 - let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize) + var summedRatios: CGFloat = 0.0 - var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height) - if frame.origin.x + frame.size.width >= maxWidth - 2.0 { - frame.size.width = max(1.0, maxWidth - frame.origin.x) + var j = i + var n = i + row.count + + while j < n { + let aspectRatio: CGFloat + if let dimensions = files[j].media.dimensions { + aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height + } else { + aspectRatio = 1.0 + } + + summedRatios += aspectRatio + + j += 1 } - displayItems.append(VisibleVideoItem(fileReference: self.files[j], frame: frame)) + var rowSize = drawableSize.width - (CGFloat(row.count - 1) * minimumInteritemSpacing) - offset.x += actualSize.width + minimumInteritemSpacing - previousItemSize = actualSize.height - contentMaxValueInScrollDirection = frame.maxY + if rowIndex == partition.count - 1 { + if row.count < 2 { + rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing) + } else if row.count < 3 { + rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing) + } + } - j += 1 + j = i + n = i + row.count + + while j < n { + let aspectRatio: CGFloat + if let dimensions = files[j].media.dimensions { + aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height + } else { + aspectRatio = 1.0 + } + let preferredAspectRatio = aspectRatio + + let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize) + + var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height) + if frame.origin.x + frame.size.width >= maxWidth - 2.0 { + frame.size.width = max(1.0, maxWidth - frame.origin.x) + } + + displayItems.append(VisibleVideoItem(fileReference: files[j], frame: frame, isTrending: isTrending)) + + offset.x += actualSize.width + minimumInteritemSpacing + previousItemSize = actualSize.height + verticalOffset = frame.maxY + + j += 1 + } + + if row.count > 0 { + offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing) + } + + i += row.count } - - if row.count > 0 { - offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing) - } - - i += row.count } - let contentSize = CGSize(width: drawableSize.width, height: contentMaxValueInScrollDirection + self.bottomInset) + + if !self.files.saved.isEmpty { + self.savedTitleNode.isHidden = false + let leftInset: CGFloat = 10.0 + let savedTitleSize = self.savedTitleNode.updateLayout(CGSize(width: drawableSize.width - leftInset * 2.0, height: 100.0)) + self.savedTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: savedTitleSize) + verticalOffset += savedTitleSize.height + 5.0 + commitFilesSpans(files: self.files.saved, isTrending: false) + //commitFiles(files: self.files.saved, isTrending: false) + } else { + self.savedTitleNode.isHidden = true + } + if !self.files.trending.isEmpty { + self.trendingHeaderNode.isHidden = false + let trendingHeight = self.trendingHeaderNode.update(theme: self.theme, strings: self.strings, width: drawableSize.width, sideInset: 0.0) + self.trendingHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: drawableSize.width, height: trendingHeight)) + verticalOffset += trendingHeight + commitFilesSpans(files: self.files.trending, isTrending: true) + //commitFiles(files: self.files.trending, isTrending: true) + } else { + self.trendingHeaderNode.isHidden = true + } + + let contentSize = CGSize(width: drawableSize.width, height: verticalOffset + self.bottomInset) self.scrollNode.view.contentSize = contentSize self.displayItems = displayItems self.validVisibleItemsOffset = nil - self.updateImmediatelyVisibleItems(ensureFrames: true) + self.currentExtendSizeForTransition = extendSizeForTransition + self.updateImmediatelyVisibleItems(ensureFrames: true, synchronous: synchronous) + + transition.updateAlpha(node: scrollNode, alpha: 1.0, force: true, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.currentExtendSizeForTransition = 0.0 + strongSelf.updateImmediatelyVisibleItems() + }) } } diff --git a/submodules/TelegramUI/Sources/PaneSearchBarNode.swift b/submodules/TelegramUI/Sources/PaneSearchBarNode.swift index 76bd5d7618..6e430c85ab 100644 --- a/submodules/TelegramUI/Sources/PaneSearchBarNode.swift +++ b/submodules/TelegramUI/Sources/PaneSearchBarNode.swift @@ -435,7 +435,7 @@ class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate { snapshot.frame = CGRect(origin: self.textField.placeholderLabel.frame.origin, size: node.labelNode.frame.size) self.textField.layer.addSublayer(snapshot) snapshot.animateAlpha(from: 0.0, to: 1.0, duration: duration * 2.0 / 3.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue) - self.textField.placeholderLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 3.0 / 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false) + //self.textField.placeholderLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 3.0 / 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false) } @@ -491,4 +491,9 @@ class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate { self.textFieldDidChange(self.textField) } } + + func updateQuery(_ query: String) { + self.textField.text = query + self.textFieldDidChange(self.textField) + } } diff --git a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift index d56bb6e555..03381f4efd 100644 --- a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift @@ -83,6 +83,12 @@ final class PaneSearchContainerNode: ASDisplayNode { } self.updateThemeAndStrings(theme: theme, strings: strings) + + if let contentNode = self.contentNode as? GifPaneSearchContentNode { + contentNode.requestUpdateQuery = { [weak self] query in + self?.updateQuery(query) + } + } } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { @@ -100,6 +106,10 @@ final class PaneSearchContainerNode: ASDisplayNode { self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor) } + func updateQuery(_ query: String) { + self.searchBar.updateQuery(query) + } + func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? { return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight)) } @@ -121,15 +131,25 @@ final class PaneSearchContainerNode: ASDisplayNode { self.searchBar.deactivate(clear: true) } - func animateIn(from placeholder: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { - let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) - let verticalOrigin = placeholderFrame.minY - 4.0 - self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition) + func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y + if let placeholder = placeholder { + let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) + verticalOrigin = placeholderFrame.minY - 4.0 + self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition) + } else { + self.contentNode.animateIn(additivePosition: 0.0, 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, completion: completion) + if let placeholder = placeholder { + self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction, completion: completion) + } else { + self.searchBar.alpha = 0.0 + transition.updateAlpha(node: self.searchBar, alpha: 1.0) + } 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) diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index d35c85f27e..20e6ce8ed9 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -56,6 +56,8 @@ private final class VisualMediaItemNode: ASDisplayNode { private var item: (VisualMediaItem, Media?, CGSize, CGSize?)? private var theme: PresentationTheme? + private var hasVisibility: Bool = false + init(context: AccountContext, interaction: VisualMediaItemInteraction) { self.context = context self.interaction = interaction @@ -192,7 +194,7 @@ private final class VisualMediaItemNode: ASDisplayNode { } else { sampleBufferLayer = takeSampleBufferLayer() self.sampleBufferLayer = sampleBufferLayer - self.containerNode.layer.insertSublayer(sampleBufferLayer.layer, above: self.imageNode.layer) + self.imageNode.layer.addSublayer(sampleBufferLayer.layer) } self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer) @@ -327,6 +329,7 @@ private final class VisualMediaItemNode: ASDisplayNode { } func updateIsVisible(_ isVisible: Bool) { + self.hasVisibility = isVisible if let _ = self.videoLayerFrameManager { let displayLink: ConstantDisplayLinkAnimator if let current = self.displayLink { @@ -342,8 +345,8 @@ private final class VisualMediaItemNode: ASDisplayNode { displayLink.frameInterval = 2 self.displayLink = displayLink } - displayLink.isPaused = !isVisible } + self.displayLink?.isPaused = !self.hasVisibility || self.isHidden } func updateSelectionState(animated: Bool) { @@ -420,6 +423,7 @@ private final class VisualMediaItemNode: ASDisplayNode { } else { self.isHidden = false } + self.displayLink?.isPaused = !self.hasVisibility || self.isHidden } } diff --git a/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift b/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift index 61c1e700f0..159126d08e 100644 --- a/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift +++ b/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift @@ -63,7 +63,7 @@ final class SoftwareVideoLayerFrameManager { func start() { let secondarySignal: Signal if let secondaryResource = self.secondaryResource { - secondarySignal = self.account.postbox.mediaBox.resourceData(self.resource, option: .complete(waitUntilFetchStatus: false)) + secondarySignal = self.account.postbox.mediaBox.resourceData(secondaryResource, option: .complete(waitUntilFetchStatus: false)) |> map { data -> String? in if data.complete { return data.path diff --git a/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift b/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift index 174c97d1c5..6c2756ec97 100644 --- a/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift +++ b/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift @@ -23,7 +23,7 @@ final class SoftwareVideoThumbnailLayer: CALayer { } } - init(account: Account, fileReference: FileMediaReference) { + init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool) { super.init() self.backgroundColor = UIColor.clear.cgColor @@ -31,7 +31,7 @@ final class SoftwareVideoThumbnailLayer: CALayer { self.masksToBounds = true if let dimensions = fileReference.media.dimensions { - self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference)).start(next: { [weak self] transform in + self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference, synchronousLoad: synchronousLoad)).start(next: { [weak self] transform in var boundingSize = dimensions.cgSize.aspectFilled(CGSize(width: 93.0, height: 93.0)) let imageSize = boundingSize boundingSize.width = min(200.0, boundingSize.width)