diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index dcda2cc19f..13cfd291de 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -1305,14 +1305,14 @@ public func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaRef } } -public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, overlayColor: overlayColor) +public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, overlayColor: overlayColor, nilForEmptyResult: nilForEmptyResult) |> map { return $0.1 } } -public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { +public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal: Signal?, Bool>, NoError> if let imageReference = imageReference { signal = chatMessagePhotoDatas(postbox: postbox, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad) @@ -1346,6 +1346,12 @@ public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: File } return nil }, { arguments in + if nilForEmptyResult { + if thumbnailData == nil && fullSizeData == nil { + return nil + } + } + let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect diff --git a/submodules/Postbox/Sources/MediaBoxFile.swift b/submodules/Postbox/Sources/MediaBoxFile.swift index 42aee7aa92..2a013197ad 100644 --- a/submodules/Postbox/Sources/MediaBoxFile.swift +++ b/submodules/Postbox/Sources/MediaBoxFile.swift @@ -131,7 +131,7 @@ private final class MediaBoxFileMap { self.progress = nil } - fileprivate func contains(_ range: Range) -> Bool { + fileprivate func contains(_ range: Range) -> Range? { let maxValue: Int if let truncationSize = self.truncationSize { maxValue = Int(truncationSize) @@ -139,7 +139,11 @@ private final class MediaBoxFileMap { maxValue = Int.max } let intRange: Range = Int(range.lowerBound) ..< min(maxValue, Int(range.upperBound)) - return self.ranges.contains(integersIn: intRange) + if self.ranges.contains(integersIn: intRange) { + return Int32(intRange.lowerBound) ..< Int32(intRange.upperBound) + } else { + return nil + } } } @@ -385,7 +389,7 @@ final class MediaBoxPartialFile { } var isCompleted = false - if let truncationSize = self.fileMap.truncationSize, self.fileMap.contains(0 ..< truncationSize) { + if let truncationSize = self.fileMap.truncationSize, let _ = self.fileMap.contains(0 ..< truncationSize) { isCompleted = true } @@ -432,9 +436,9 @@ final class MediaBoxPartialFile { func read(range: Range) -> Data? { assert(self.queue.isCurrent()) - if self.fileMap.contains(range) { - self.fd.seek(position: Int64(range.lowerBound)) - var data = Data(count: range.count) + if let actualRange = self.fileMap.contains(range) { + self.fd.seek(position: Int64(actualRange.lowerBound)) + var data = Data(count: actualRange.count) let dataCount = data.count let readBytes = data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Int in return self.fd.read(bytes, dataCount) @@ -452,8 +456,8 @@ final class MediaBoxPartialFile { func data(range: Range, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable { assert(self.queue.isCurrent()) - if self.fileMap.contains(range) { - next(MediaResourceData(path: self.path, offset: Int(range.lowerBound), size: range.count, complete: true)) + if let actualRange = self.fileMap.contains(range) { + next(MediaResourceData(path: self.path, offset: Int(actualRange.lowerBound), size: actualRange.count, complete: true)) return EmptyDisposable } @@ -481,7 +485,7 @@ final class MediaBoxPartialFile { func fetched(range: Range, priority: MediaBoxFetchPriority, fetch: @escaping (Signal<[(Range, MediaBoxFetchPriority)], NoError>) -> Signal, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable { assert(self.queue.isCurrent()) - if self.fileMap.contains(range) { + if let _ = self.fileMap.contains(range) { completed() return EmptyDisposable } @@ -559,7 +563,7 @@ final class MediaBoxPartialFile { assert(self.queue.isCurrent()) next(self.fileMap.ranges) - if let truncationSize = self.fileMap.truncationSize, self.fileMap.contains(0 ..< truncationSize) { + if let truncationSize = self.fileMap.truncationSize, let _ = self.fileMap.contains(0 ..< truncationSize) { completed() return EmptyDisposable } @@ -676,8 +680,8 @@ final class MediaBoxPartialFile { if request.waitingUntilAfterInitialFetch { request.waitingUntilAfterInitialFetch = false - if strongSelf.fileMap.contains(request.range) { - request.completion(MediaResourceData(path: strongSelf.path, offset: Int(request.range.lowerBound), size: request.range.count, complete: true)) + if let actualRange = strongSelf.fileMap.contains(request.range) { + request.completion(MediaResourceData(path: strongSelf.path, offset: Int(actualRange.lowerBound), size: actualRange.count, complete: true)) } else { request.completion(MediaResourceData(path: strongSelf.path, offset: Int(request.range.lowerBound), size: 0, complete: false)) } diff --git a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift index 656d822a65..329098c6c5 100644 --- a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift +++ b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift @@ -123,6 +123,7 @@ public final class ShimmerEffectNode: ASDisplayNode { case circle(CGRect) case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat) case roundedRect(rect: CGRect, cornerRadius: CGFloat) + case rect(rect: CGRect) } private let backgroundNode: ASDisplayNode @@ -189,6 +190,8 @@ public final class ShimmerEffectNode: ASDisplayNode { UIGraphicsPushContext(context) path.fill() UIGraphicsPopContext() + case let .rect(rect): + context.fill(rect) } } }) diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index 125936cff6..1a6e7c31c4 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -334,12 +334,18 @@ public func chatMessageSticker(account: Account, file: TelegramMediaFile, small: return chatMessageSticker(postbox: account.postbox, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad) } -public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaResource, animated: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaResource, animated: Bool = false, synchronousLoad: Bool = false, nilIfEmpty: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessageStickerPackThumbnailData(postbox: postbox, resource: resource, animated: animated, synchronousLoad: synchronousLoad) return signal |> map { fullSizeData in return { arguments in + if nilIfEmpty { + if fullSizeData == nil { + return nil + } + } + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil) let drawingRect = arguments.drawingRect diff --git a/submodules/TelegramCore/Sources/RequestChatContextResults.swift b/submodules/TelegramCore/Sources/RequestChatContextResults.swift index 680717dc13..f01e460437 100644 --- a/submodules/TelegramCore/Sources/RequestChatContextResults.swift +++ b/submodules/TelegramCore/Sources/RequestChatContextResults.swift @@ -111,7 +111,7 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P } return account.postbox.transaction { transaction -> ChatContextResultCollection? in - if result.cacheTimeout > 10 && offset.isEmpty { + if result.cacheTimeout > 10 { 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) { diff --git a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift index a0af5b84c8..318e466a46 100644 --- a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift @@ -61,7 +61,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont private let imageNodeBackground: ASDisplayNode private let imageNode: TransformImageNode - private var videoLayer: (SoftwareVideoThumbnailLayer, SoftwareVideoLayerFrameManager, SampleBufferLayer)? + private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)? private var currentImageResource: TelegramMediaResource? private var currentVideoFile: TelegramMediaFile? @@ -248,13 +248,13 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont if updatedVideoFile { if let (thumbnailLayer, _, layer) = self.videoLayer { self.videoLayer = nil - thumbnailLayer.removeFromSuperlayer() + thumbnailLayer.removeFromSupernode() layer.layer.removeFromSuperlayer() } if let videoFileReference = videoFileReference { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: videoFileReference, synchronousLoad: false) - self.layer.addSublayer(thumbnailLayer) + let thumbnailLayer = SoftwareVideoThumbnailNode(account: self.account, fileReference: videoFileReference, synchronousLoad: false) + self.addSubnode(thumbnailLayer) let layerHolder = takeSampleBufferLayer() layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill self.layer.addSublayer(layerHolder.layer) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift index bfbca28bcb..ab06c54cf0 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift @@ -24,6 +24,16 @@ private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) { } } +final class ChatMediaInputGifPaneTrendingState { + let files: [MultiplexedVideoNodeFile] + let nextOffset: String? + + init(files: [MultiplexedVideoNodeFile], nextOffset: String?) { + self.files = files + self.nextOffset = nextOffset + } +} + final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private let account: Account private var theme: PresentationTheme @@ -49,7 +59,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private let emptyNode: ImmediateTextNode private let disposable = MetaDisposable() - let trendingPromise = Promise<[MultiplexedVideoNodeFile]?>(nil) + let trendingPromise = Promise(nil) private var validLayout: (CGSize, CGFloat, CGFloat, Bool, Bool, DeviceMetrics)? private var didScrollPreviousOffset: CGFloat? @@ -57,6 +67,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private var didScrollPreviousState: ChatMediaInputPaneScrollState? private(set) var mode: ChatMediaInputGifMode = .recent + private var isLoadingMore: Bool = false + private var nextOffset: String? init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void) { self.account = account @@ -132,7 +144,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { return } self.mode = mode - self.resetMode(synchronous: true) + self.resetMode(synchronous: true, searchOffset: nil) } override var isEmpty: Bool { @@ -160,7 +172,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { let displaySearch: Bool switch self.mode { - case .recent: + case .recent, .trending: displaySearch = true default: displaySearch = false @@ -194,9 +206,9 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { func initializeIfNeeded() { if self.multiplexedNode == nil { self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) - |> map { items -> [MultiplexedVideoNodeFile]? in - if let (items, _) = items { - return items + |> map { items -> ChatMediaInputGifPaneTrendingState? in + if let items = items { + return ChatMediaInputGifPaneTrendingState(files: items.files, nextOffset: items.nextOffset) } else { return nil } @@ -243,6 +255,10 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { let state = ChatMediaInputPaneScrollState(absoluteOffset: absoluteOffset, relativeChange: delta) strongSelf.didScrollPreviousState = state strongSelf.paneDidScroll(strongSelf, state, .immediate) + + if offset >= height - multiplexedNode.bounds.height - 200.0 { + strongSelf.loadMore() + } } multiplexedNode.didEndScrolling = { [weak self] in @@ -253,23 +269,25 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { strongSelf.fixPaneScroll(strongSelf, didScrollPreviousState) } - if let multiplexedNode = strongSelf.multiplexedNode { + if let _ = strongSelf.multiplexedNode { //fixListScrolling(multiplexedNode) } } self.updateMultiplexedNodeLayout(changedIsExpanded: false, transition: .immediate) - self.resetMode(synchronous: false) + self.resetMode(synchronous: false, searchOffset: nil) } } - private func resetMode(synchronous: Bool) { - let filesSignal: Signal + private func resetMode(synchronous: Bool, searchOffset: String?) { + self.isLoadingMore = true + + let filesSignal: Signal<(MultiplexedVideoNodeFiles, String?), NoError> switch self.mode { case .recent: filesSignal = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)])) - |> map { trending, view -> MultiplexedVideoNodeFiles in + |> map { trending, view -> (MultiplexedVideoNodeFiles, String?) in var recentGifs: OrderedItemListView? if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] { recentGifs = orderedView as? OrderedItemListView @@ -286,36 +304,58 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { saved = [] } - return MultiplexedVideoNodeFiles(saved: saved, trending: trending ?? [], isSearch: false) + return (MultiplexedVideoNodeFiles(saved: saved, trending: trending?.files ?? [], isSearch: false, canLoadMore: false), nil) } case .trending: - filesSignal = self.trendingPromise.get() - |> map { trending -> MultiplexedVideoNodeFiles in - return MultiplexedVideoNodeFiles(saved: [], trending: trending ?? [], isSearch: true) + if let searchOffset = searchOffset { + filesSignal = paneGifSearchForQuery(account: self.account, query: "", offset: searchOffset, incompleteResults: true, delayRequest: false, updateActivity: nil) + |> map { result -> (MultiplexedVideoNodeFiles, String?) in + let canLoadMore: Bool + if let result = result { + canLoadMore = !result.isComplete + } else { + canLoadMore = true + } + return (MultiplexedVideoNodeFiles(saved: [], trending: result?.files ?? [], isSearch: true, canLoadMore: canLoadMore), result?.nextOffset) + } + } else { + filesSignal = self.trendingPromise.get() + |> map { trending -> (MultiplexedVideoNodeFiles, String?) in + return (MultiplexedVideoNodeFiles(saved: [], trending: trending?.files ?? [], isSearch: true, canLoadMore: false), trending?.nextOffset) + } } case let .emojiSearch(emoji): - filesSignal = paneGifSearchForQuery(account: self.account, query: emoji, offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) - |> map { trending -> MultiplexedVideoNodeFiles in - return MultiplexedVideoNodeFiles(saved: [], trending: trending?.0 ?? [], isSearch: true) + filesSignal = paneGifSearchForQuery(account: self.account, query: emoji, offset: searchOffset, incompleteResults: true, delayRequest: false, updateActivity: nil) + |> map { result -> (MultiplexedVideoNodeFiles, String?) in + let canLoadMore: Bool + if let result = result { + canLoadMore = !result.isComplete + } else { + canLoadMore = true + } + return (MultiplexedVideoNodeFiles(saved: [], trending: result?.files ?? [], isSearch: true, canLoadMore: canLoadMore), result?.nextOffset) } } var firstTime = true self.disposable.set((filesSignal - |> deliverOnMainQueue).start(next: { [weak self] files in + |> deliverOnMainQueue).start(next: { [weak self] addedFiles, nextOffset in if let strongSelf = self { - //let previousFiles = strongSelf.multiplexedNode?.files var resetScrollingToOffset: CGFloat? if firstTime { firstTime = false - resetScrollingToOffset = 0.0 + if searchOffset == nil { + resetScrollingToOffset = 0.0 + } } + strongSelf.isLoadingMore = false + let displaySearch: Bool switch strongSelf.mode { - case .recent: + case .recent, .trending: displaySearch = true default: displaySearch = false @@ -327,17 +367,41 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { strongSelf.multiplexedNode?.topInset = topInset + (displaySearch ? 60.0 : 0.0) } - strongSelf.multiplexedNode?.setFiles(files: files, synchronous: synchronous, resetScrollingToOffset: resetScrollingToOffset) - - /*let wasEmpty: Bool - if let previousFiles = previousFiles { - wasEmpty = previousFiles.trending.isEmpty && previousFiles.saved.isEmpty - } else { - wasEmpty = true + var files = addedFiles + if let _ = searchOffset { + var resultFiles: [MultiplexedVideoNodeFile] = [] + if let currentFiles = strongSelf.multiplexedNode?.files.trending { + resultFiles = currentFiles + } + var existingFileIds = Set(resultFiles.map { $0.file.media.fileId }) + for file in addedFiles.trending { + if existingFileIds.contains(file.file.media.fileId) { + continue + } + existingFileIds.insert(file.file.media.fileId) + resultFiles.append(file) + } + files = MultiplexedVideoNodeFiles(saved: [], trending: resultFiles, isSearch: true, canLoadMore: addedFiles.canLoadMore) } - let isEmpty = files.trending.isEmpty && files.saved.isEmpty - strongSelf.emptyNode.isHidden = !isEmpty*/ + + strongSelf.nextOffset = nextOffset + strongSelf.multiplexedNode?.setFiles(files: files, synchronous: synchronous, resetScrollingToOffset: resetScrollingToOffset) } })) } + + private func loadMore() { + if self.isLoadingMore { + return + } + guard let nextOffset = self.nextOffset else { + return + } + switch self.mode { + case .trending, .emojiSearch: + self.resetMode(synchronous: false, searchOffset: nextOffset) + default: + break + } + } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift index afdd2aac25..d43f7d2890 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift @@ -24,7 +24,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { let selectedItem: () -> Void var selectable: Bool { - return false + return true } init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, selected: @escaping () -> Void) { @@ -37,14 +37,16 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = ChatMediaInputMetaSectionItemNode() - node.contentSize = CGSize(width: 41.0, height: 41.0) - node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) - node.inputNodeInteraction = self.inputNodeInteraction - node.setItem(item: self) - node.updateTheme(theme: self.theme) - node.updateIsHighlighted() - node.updateAppearanceTransition(transition: .immediate) Queue.mainQueue().async { + node.inputNodeInteraction = self.inputNodeInteraction + node.setItem(item: self) + node.updateTheme(theme: self.theme) + node.updateIsHighlighted() + node.updateAppearanceTransition(transition: .immediate) + + node.contentSize = CGSize(width: 41.0, height: 41.0) + node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) + completion(node, { return (nil, { _ in @@ -78,7 +80,6 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { private let textNodeContainer: ASDisplayNode private let textNode: ImmediateTextNode private let highlightNode: ASImageNode - private let buttonNode: HighlightTrackingButtonNode var item: ChatMediaInputMetaSectionItem? var currentCollectionId: ItemCollectionId? @@ -110,32 +111,22 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { self.textNodeContainer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - self.buttonNode = HighlightTrackingButtonNode() - super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.highlightNode) self.addSubnode(self.imageNode) self.addSubnode(self.textNodeContainer) - self.addSubnode(self.buttonNode) let imageSize = CGSize(width: 26.0, height: 26.0) self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) - self.textNodeContainer.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) - - self.buttonNode.frame = CGRect(origin: CGPoint(), size: boundingSize) - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.textNodeContainer.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + 1.0), size: imageSize) } override func didLoad() { super.didLoad() } - @objc private func buttonPressed() { - self.item?.selectedItem() - } - func setItem(item: ChatMediaInputMetaSectionItem) { self.item = item switch item.type { @@ -167,7 +158,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme) case let .gifEmoji(emoji): self.imageNode.image = nil - self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(28.0), textColor: .black) + self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(27.0), textColor: .black) let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0)) self.textNode.frame = CGRect(origin: CGPoint(x: floor((self.textNodeContainer.bounds.width - textSize.width) / 2.0), y: floor((self.textNodeContainer.bounds.height - textSize.height) / 2.0)), size: textSize) } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 931f3b44e3..3f983630f0 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -501,9 +501,11 @@ final class ChatMediaInputNode: ChatInputNode { self.listView = ListView() self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) + self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false self.gifListView = ListView() self.gifListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) + self.gifListView.scroller.panGestureRecognizer.cancelsTouchesInView = false var paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)? var fixPaneScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void)? diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index a29b303e62..c4f85c3c39 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -11,6 +11,7 @@ import StickerResources import AccountContext import AnimatedStickerNode import TelegramAnimatedStickerNode +import ShimmerEffect enum ChatMediaInputStickerGridSectionAccessory { case none @@ -121,6 +122,7 @@ final class ChatMediaInputStickerGridItem: GridItem { let selected: () -> Void let interfaceInteraction: ChatControllerInteraction? let inputNodeInteraction: ChatMediaInputNodeInteraction + let theme: PresentationTheme let section: GridSection? @@ -130,6 +132,7 @@ final class ChatMediaInputStickerGridItem: GridItem { self.stickerItem = stickerItem self.interfaceInteraction = interfaceInteraction self.inputNodeInteraction = inputNodeInteraction + self.theme = theme self.selected = selected if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue { self.section = nil @@ -170,6 +173,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { private var currentSize: CGSize? private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? + private var placeholderNode: ShimmerEffectNode? private var didSetUpAnimationNode = false private var item: ChatMediaInputStickerGridItem? @@ -196,16 +200,45 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { override init() { self.imageNode = TransformImageNode() + self.placeholderNode = ShimmerEffectNode() super.init() self.addSubnode(self.imageNode) + if let placeholderNode = self.placeholderNode { + self.addSubnode(placeholderNode) + } + + var firstTime = true + self.imageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) + } + firstTime = false + } } deinit { self.stickerFetchedDisposable.dispose() } + private func removePlaceholder(animated: Bool) { + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + if !animated { + placeholderNode.removeFromSupernode() + } else { + placeholderNode.alpha = 0.0 + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in + placeholderNode?.removeFromSupernode() + }) + } + } + } + override func didLoad() { super.didLoad() @@ -216,7 +249,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { guard let item = item as? ChatMediaInputStickerGridItem else { return } + + let sideSize: CGFloat = size.width - 10.0 + let boundingSize = CGSize(width: sideSize, height: sideSize) + self.item = item + + if self.currentState == nil || self.currentState!.0 !== item.account || self.currentState!.1 != item.stickerItem { if let dimensions = item.stickerItem.file.dimensions { if item.stickerItem.file.isAnimatedSticker { @@ -227,7 +266,11 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { animationNode.started = { [weak self] in self?.imageNode.isHidden = true } - self.addSubnode(animationNode) + if let placeholderNode = self.placeholderNode { + self.insertSubnode(animationNode, belowSubnode: placeholderNode) + } else { + self.addSubnode(animationNode) + } } let dimensions = item.stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.account.postbox, file: item.stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))) @@ -253,9 +296,6 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if self.currentSize != size { self.currentSize = size - let sideSize: CGFloat = size.width - 10.0 //min(75.0 - 10.0, size.width) - let boundingSize = CGSize(width: sideSize, height: sideSize) - if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() @@ -266,6 +306,20 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { } } } + + if let placeholderNode = self.placeholderNode { + let placeholderFrame = CGRect(origin: CGPoint(x: floor((size.width - boundingSize.width) / 2.0), y: floor((size.height - boundingSize.height) / 2.0)), size: boundingSize) + placeholderNode.frame = CGRect(origin: CGPoint(), size: size) + + let theme = item.theme + placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.mixedWith(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.9), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.roundedRect(rect: placeholderFrame, cornerRadius: 10.0)], size: bounds.size) + } + } + + override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + if let placeholderNode = self.placeholderNode { + placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize) + } } @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift index 7afd868d0b..8494e605d0 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift @@ -11,6 +11,7 @@ import StickerResources import ItemListStickerPackItem import AnimatedStickerNode import TelegramAnimatedStickerNode +import ShimmerEffect final class ChatMediaInputStickerPackItem: ListViewItem { let account: Account @@ -75,6 +76,8 @@ private let verticalOffset: CGFloat = 3.0 final class ChatMediaInputStickerPackItemNode: ListViewItemNode { private let imageNode: TransformImageNode private var animatedStickerNode: AnimatedStickerNode? + private var placeholderNode: ShimmerEffectNode? + private var placeholderImageNode: ASImageNode? private let highlightNode: ASImageNode var inputNodeInteraction: ChatMediaInputNodeInteraction? @@ -107,7 +110,12 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { self.imageNode = TransformImageNode() self.imageNode.isLayerBacked = !smartInvertColorsEnabled() - self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) + self.placeholderImageNode = ASImageNode() + self.placeholderImageNode?.isUserInteractionEnabled = false + + //self.placeholderNode = ShimmerEffectNode() + + self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset - UIScreenPixel, y: floor((boundingSize.height - highlightSize.height) / 2.0) - UIScreenPixel), size: highlightSize) self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.imageNode.contentAnimations = [.firstUpdate] @@ -116,12 +124,54 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { self.addSubnode(self.highlightNode) self.addSubnode(self.imageNode) + if let placeholderImageNode = self.placeholderImageNode { + self.addSubnode(placeholderImageNode) + } + if let placeholderNode = self.placeholderNode { + self.addSubnode(placeholderNode) + } + + var firstTime = true + self.imageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) + } + firstTime = false + } } deinit { self.stickerFetchedDisposable.dispose() } + private func removePlaceholder(animated: Bool) { + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + if !animated { + placeholderNode.removeFromSupernode() + } else { + placeholderNode.alpha = 0.0 + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in + placeholderNode?.removeFromSupernode() + }) + } + } + if let placeholderImageNode = self.placeholderImageNode { + self.placeholderImageNode = nil + if !animated { + placeholderImageNode.removeFromSupernode() + } else { + placeholderImageNode.alpha = 0.0 + placeholderImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderImageNode] _ in + placeholderImageNode?.removeFromSupernode() + }) + } + } + } + func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, theme: PresentationTheme) { self.currentCollectionId = collectionId @@ -159,13 +209,13 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { let imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize) let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) imageApply() - self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource)) + self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource, nilIfEmpty: true)) self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) case let .animated(resource): let imageSize = boundingImageSize let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) imageApply() - self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true)) + self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true, nilIfEmpty: true)) self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false @@ -178,7 +228,11 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { animatedStickerNode = AnimatedStickerNode() self.animatedStickerNode = animatedStickerNode animatedStickerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - self.addSubnode(animatedStickerNode) + if let placeholderNode = self.placeholderNode { + self.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode) + } else { + self.addSubnode(animatedStickerNode) + } animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 80, height: 80, mode: .cached) } animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers @@ -191,10 +245,35 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { } } + if let placeholderImageNode = self.placeholderImageNode { + if placeholderImageNode.image == nil { + placeholderImageNode.image = generateStretchableFilledCircleImage(diameter: 10.0, color: theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.mixedWith(.clear, alpha: 0.6)) + } + let size = boundingSize + let imageSize = boundingImageSize + let placeholderFrame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) + placeholderImageNode.frame = placeholderFrame + } + + if let placeholderNode = self.placeholderNode { + let size = boundingSize + let imageSize = boundingImageSize + let placeholderFrame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) + placeholderNode.frame = CGRect(origin: CGPoint(), size: size) + + placeholderNode.update(backgroundColor: theme.chat.inputPanel.panelBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.mixedWith(theme.chat.inputPanel.panelBackgroundColor, alpha: 0.8), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.roundedRect(rect: placeholderFrame, cornerRadius: 5.0)], size: bounds.size) + } + self.updateIsHighlighted() } } + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let placeholderNode = self.placeholderNode { + //placeholderNode.updateAbsoluteRect(rect, within: containerSize) + } + } + func updateIsHighlighted() { assert(Queue.mainQueue().isCurrent()) if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index f1c63f56e2..0ddcf73ac6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -550,6 +550,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio replaceVideoNode = true } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { replaceVideoNode = true + } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { + replaceVideoNode = true } } } else if !(file.resource is LocalFileVideoMediaResource) { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 4333403f2b..bc35000dbe 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1433,7 +1433,7 @@ 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)) + let searchActivityIndicator = ActivityIndicator(type: .custom(currentState.theme.list.itemAccentColor, 11.0, 1.0, false)) searchActivityIndicator.isUserInteractionEnabled = false self.searchActivityIndicator = searchActivityIndicator let indicatorSize = searchActivityIndicator.measure(CGSize(width: 100.0, height: 100.0)) @@ -1441,7 +1441,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { 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) + //searchActivityIndicator.layer.sublayerTransform = CATransform3DMakeScale(0.5, 0.5, 1.0) } } else if let searchActivityIndicator = self.searchActivityIndicator { self.searchActivityIndicator = nil diff --git a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift index 6f397c1d61..064b575d51 100644 --- a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift @@ -11,7 +11,19 @@ import AccountContext import WebSearchUI import AppBundle -func paneGifSearchForQuery(account: Account, query: String, offset: String?, incompleteResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> { +class PaneGifSearchForQueryResult { + let files: [MultiplexedVideoNodeFile] + let nextOffset: String? + let isComplete: Bool + + init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool) { + self.files = files + self.nextOffset = nextOffset + self.isComplete = isComplete + } +} + +func paneGifSearchForQuery(account: Account, query: String, offset: String?, incompleteResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal { let contextBot = account.postbox.transaction { transaction -> String in let configuration = currentSearchBotsConfiguration(transaction: transaction) return configuration.gifBotUsername ?? "gif" @@ -30,16 +42,14 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, inc return .single(nil) } } - |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in + |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool), 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, offset: offset ?? "", incompleteResults: incompleteResults, limit: 1) - |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - return { _ in - return .contextRequestResult(user, results) - } + |> map { results -> (ChatPresentationInputQueryResult?, Bool) in + return (.contextRequestResult(user, results), results != nil) } - let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> + let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool), NoError> if delayRequest { maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue()) } else { @@ -48,12 +58,12 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, inc return maybeDelayedContextResults } else { - return .single({ _ in return nil }) + return .single((nil, true)) } } return contextBot - |> mapToSignal { result -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> in - if let r = result(nil), case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection { + |> mapToSignal { result -> Signal in + if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection { let results = collection.results var references: [MultiplexedVideoNodeFile] = [] for result in results { @@ -101,7 +111,7 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, inc } } } - return .single((references, collection.nextOffset)) + return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1)) } else if incompleteResults { return .single(nil) } else { @@ -134,7 +144,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { private var validLayout: CGSize? - private let trendingPromise: Promise<[MultiplexedVideoNodeFile]?> + private let trendingPromise: Promise private let searchDisposable = MetaDisposable() private let _ready = Promise() @@ -149,7 +159,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { private var hasInitialText = false - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[MultiplexedVideoNodeFile]?>) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise) { self.context = context self.controllerInteraction = controllerInteraction self.inputNodeInteraction = inputNodeInteraction @@ -190,12 +200,19 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError> if !text.isEmpty { signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: "", updateActivity: self.updateActivity) + |> map { result -> ([MultiplexedVideoNodeFile], String?)? in + if let result = result { + return (result.files, result.nextOffset) + } else { + return nil + } + } self.updateActivity?(true) } else { signal = self.trendingPromise.get() |> map { items -> ([MultiplexedVideoNodeFile], String?)? in if let items = items { - return (items, nil) + return (items.files, nil) } else { return nil } @@ -215,7 +232,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { } else { strongSelf.nextOffset = nil } - strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: result, isSearch: true), synchronous: true, resetScrollingToOffset: nil) + strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: result, isSearch: true, canLoadMore: false), synchronous: true, resetScrollingToOffset: nil) strongSelf.updateActivity?(false) strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty })) @@ -232,6 +249,13 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError> signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity) + |> map { result -> ([MultiplexedVideoNodeFile], String?)? in + if let result = result { + return (result.files, result.nextOffset) + } else { + return nil + } + } self.searchDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] result in @@ -255,7 +279,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { } else { strongSelf.nextOffset = nil } - strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: files, isSearch: true), synchronous: true, resetScrollingToOffset: nil) + strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: files, isSearch: true, canLoadMore: false), synchronous: true, resetScrollingToOffset: nil) strongSelf.notFoundNode.isHidden = text.isEmpty || !files.isEmpty })) } diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index edefa1d538..7037d5e141 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -84,7 +84,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode private let imageNodeBackground: ASDisplayNode private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? - private var videoLayer: (SoftwareVideoThumbnailLayer, SoftwareVideoLayerFrameManager, SampleBufferLayer)? + private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)? private var currentImageResource: TelegramMediaResource? private var currentVideoFile: TelegramMediaFile? private var currentAnimatedStickerFile: TelegramMediaFile? @@ -346,14 +346,14 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode if updatedVideoFile { if let (thumbnailLayer, _, layer) = strongSelf.videoLayer { strongSelf.videoLayer = nil - thumbnailLayer.removeFromSuperlayer() + thumbnailLayer.removeFromSupernode() layer.layer.removeFromSuperlayer() } if let videoFile = videoFile { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads) + let thumbnailLayer = SoftwareVideoThumbnailNode(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) + strongSelf.addSubnode(thumbnailLayer) let layerHolder = takeSampleBufferLayer() layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) diff --git a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift index 55453f9771..e9a237d4e9 100644 --- a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift +++ b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift @@ -9,6 +9,34 @@ import SyncCore import AVFoundation import ContextUI import TelegramPresentationData +import ShimmerEffect + +final class MultiplexedVideoPlaceholderNode: ASDisplayNode { + private let effectNode: ShimmerEffectNode + private var theme: PresentationTheme? + private var size: CGSize? + + override init() { + self.effectNode = ShimmerEffectNode() + + super.init() + + self.addSubnode(self.effectNode) + } + + func update(size: CGSize, theme: PresentationTheme) { + if self.theme === theme && self.size == size { + return + } + + self.effectNode.frame = CGRect(origin: CGPoint(), size: size) + self.effectNode.update(backgroundColor: theme.chat.inputPanel.panelBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.mixedWith(theme.chat.inputPanel.panelBackgroundColor, alpha: 0.72), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.rect(rect: CGRect(origin: CGPoint(), size: size))], size: bounds.size) + } + + func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + self.effectNode.updateAbsoluteRect(absoluteRect, within: containerSize) + } +} private final class MultiplexedVideoTrackingNode: ASDisplayNode { var inHierarchyUpdated: ((Bool) -> Void)? @@ -60,11 +88,13 @@ final class MultiplexedVideoNodeFiles { let saved: [MultiplexedVideoNodeFile] let trending: [MultiplexedVideoNodeFile] let isSearch: Bool + let canLoadMore: Bool - init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool) { + init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool, canLoadMore: Bool) { self.saved = saved self.trending = trending self.isSearch = isSearch + self.canLoadMore = canLoadMore } } @@ -95,7 +125,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } } - private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false) + private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false, canLoadMore: false) func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) { self.files = files @@ -109,15 +139,14 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } private var displayItems: [VisibleVideoItem] = [] - private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:] - private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:] + private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailNode] = [:] + private var visiblePlaceholderNodes: [Int: MultiplexedVideoPlaceholderNode] = [:] private let contextContainerNode: ContextControllerSourceNode let scrollNode: ASScrollNode private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:] - private let savedTitleNode: ImmediateTextNode private let trendingTitleNode: ImmediateTextNode private var displayLink: CADisplayLink! @@ -145,9 +174,6 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { self.contextContainerNode = ContextControllerSourceNode() self.scrollNode = ASScrollNode() - self.savedTitleNode = ImmediateTextNode() - self.savedTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_SavedSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) - self.trendingTitleNode = ImmediateTextNode() self.trendingTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_TrendingSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) @@ -158,7 +184,6 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.alwaysBounceVertical = true - self.scrollNode.addSubnode(self.savedTitleNode) self.scrollNode.addSubnode(self.trendingTitleNode) self.addSubnode(self.trackingNode) @@ -255,9 +280,6 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { deinit { self.displayLink.invalidate() self.displayLink.isPaused = true - for(_, disposable) in self.statusDisposable { - disposable.dispose() - } for (_, value) in self.visibleLayers { value.1.isFreed = true } @@ -310,9 +332,18 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { private var validVisibleItemsOffset: CGFloat? private func updateImmediatelyVisibleItems(ensureFrames: Bool = false, synchronous: Bool = false) { var visibleBounds = self.scrollNode.bounds + let containerSize = visibleBounds.size visibleBounds.size.height += max(0.0, self.currentExtendSizeForTransition) let visibleThumbnailBounds = visibleBounds.insetBy(dx: 0.0, dy: -350.0) + let containerWidth = containerSize.width + let itemSpacing: CGFloat = 1.0 + let itemsInRow = max(3, min(6, Int(containerWidth / 140.0))) + let itemSize: CGFloat = floor(containerWidth / CGFloat(itemsInRow)) + + let absoluteContainerSize = CGSize(width: containerSize.width, height: containerSize.height) + let absoluteContainerOffset = -visibleBounds.origin.y + if let validVisibleItemsOffset = self.validVisibleItemsOffset, validVisibleItemsOffset.isEqual(to: visibleBounds.origin.y) { return } @@ -326,27 +357,45 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { var visibleThumbnailIds = Set() var visibleIds = Set() - for item in self.displayItems { + var maxVisibleIndex = -1 + + for index in 0 ..< self.displayItems.count { + let item = self.displayItems[index] + if item.frame.maxY < minVisibleThumbnailY { - continue; + continue } if item.frame.minY > maxVisibleThumbnailY { - break; + break } + maxVisibleIndex = max(maxVisibleIndex, index) + visibleThumbnailIds.insert(item.id) - if let thumbnailLayer = self.visibleThumbnailLayers[item.id] { + let thumbnailLayer: SoftwareVideoThumbnailNode + if let current = self.visibleThumbnailLayers[item.id] { + thumbnailLayer = current if ensureFrames { thumbnailLayer.frame = item.frame } } else { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.file.file, synchronousLoad: synchronous) + var existingPlaceholderNode: MultiplexedVideoPlaceholderNode? + if let placeholderNode = self.visiblePlaceholderNodes[index] { + existingPlaceholderNode = placeholderNode + self.visiblePlaceholderNodes.removeValue(forKey: index) + placeholderNode.removeFromSupernode() + } + + thumbnailLayer = SoftwareVideoThumbnailNode(account: self.account, fileReference: item.file.file, synchronousLoad: synchronous, usePlaceholder: true, existingPlaceholder: existingPlaceholderNode) thumbnailLayer.frame = item.frame - self.scrollNode.layer.addSublayer(thumbnailLayer) + self.scrollNode.addSubnode(thumbnailLayer) self.visibleThumbnailLayers[item.id] = thumbnailLayer } + thumbnailLayer.update(theme: self.theme, size: item.frame.size) + thumbnailLayer.updateAbsoluteRect(item.frame.offsetBy(dx: 0.0, dy: absoluteContainerOffset), within: absoluteContainerSize) + if item.frame.maxY < minVisibleY { continue } @@ -375,6 +424,43 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } } + var visiblePlaceholderIndices = Set() + if self.files.canLoadMore { + let verticalOffset: CGFloat = self.topInset + + let sideInset: CGFloat = 0.0 + + var indexImpl = maxVisibleIndex + 1 + while true { + let index = indexImpl + indexImpl += 1 + + let rowIndex = index / Int(itemsInRow) + let columnIndex = index % Int(itemsInRow) + let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: verticalOffset + itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing)) + let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (containerWidth - itemOrigin.x) : itemSize, height: itemSize)) + if itemFrame.maxY < minVisibleY { + continue + } + if itemFrame.minY > maxVisibleY { + break + } + visiblePlaceholderIndices.insert(index) + + let placeholderNode: MultiplexedVideoPlaceholderNode + if let current = self.visiblePlaceholderNodes[index] { + placeholderNode = current + } else { + placeholderNode = MultiplexedVideoPlaceholderNode() + self.visiblePlaceholderNodes[index] = placeholderNode + self.scrollNode.addSubnode(placeholderNode) + } + placeholderNode.frame = itemFrame + placeholderNode.update(size: itemFrame.size, theme: self.theme) + placeholderNode.updateAbsoluteRect(itemFrame.offsetBy(dx: 0.0, dy: absoluteContainerOffset), within: absoluteContainerSize) + } + } + var removeIds: [VisibleVideoItem.Id] = [] for id in self.visibleLayers.keys { if !visibleIds.contains(id) { @@ -389,12 +475,12 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } } - /*var removeProgressIds: [MediaId] = [] - for id in self.visibleProgressNodes.keys { - if !visibleIds.contains(id) { - removeProgressIds.append(id) + var removePlaceholderIndices: [Int] = [] + for index in self.visiblePlaceholderNodes.keys { + if !visiblePlaceholderIndices.contains(index) { + removePlaceholderIndices.append(index) } - }*/ + } for id in removeIds { let (_, layerHolder) = self.visibleLayers[id]! @@ -404,16 +490,16 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { for id in removeThumbnailIds { let thumbnailLayer = self.visibleThumbnailLayers[id]! - thumbnailLayer.removeFromSuperlayer() + thumbnailLayer.removeFromSupernode() self.visibleThumbnailLayers.removeValue(forKey: id) } - /*for id in removeProgressIds { - let progressNode = self.visibleProgressNodes[id]! - progressNode.removeFromSupernode() - self.visibleProgressNodes.removeValue(forKey: id) - self.statusDisposable.removeValue(forKey: id)?.dispose() - }*/ + for index in removePlaceholderIndices { + if let placeholderNode = self.visiblePlaceholderNodes[index] { + placeholderNode.removeFromSupernode() + self.visiblePlaceholderNodes.removeValue(forKey: index) + } + } } private func updateVisibleItems(extendSizeForTransition: CGFloat, transition: ContainedViewLayoutTransition, synchronous: Bool = false) { @@ -423,6 +509,29 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { var verticalOffset: CGFloat = self.topInset + func commitFileGrid(files: [MultiplexedVideoNodeFile], isTrending: Bool) { + let containerWidth = drawableSize.width + let itemCount = files.count + let itemSpacing: CGFloat = 1.0 + let itemsInRow = max(3, min(6, Int(containerWidth / 140.0))) + let itemSize: CGFloat = floor(containerWidth / CGFloat(itemsInRow)) + + let rowCount = itemCount / itemsInRow + (itemCount % itemsInRow == 0 ? 0 : 1) + + let sideInset: CGFloat = 0.0 + + for index in 0 ..< itemCount { + let rowIndex = index / Int(itemsInRow) + let columnIndex = index % Int(itemsInRow) + let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: verticalOffset + itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing)) + let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (containerWidth - itemOrigin.x) : itemSize, height: itemSize)) + displayItems.append(VisibleVideoItem(file: files[index], frame: itemFrame, isTrending: isTrending)) + } + + let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize + verticalOffset += contentHeight + } + func commitFilesSpans(files: [MultiplexedVideoNodeFile], isTrending: Bool) { var rowsCount = 0 var firstRowMax = 0; @@ -529,15 +638,8 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { var hasContent = false 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) + commitFileGrid(files: self.files.saved, isTrending: false) hasContent = true - } else { - self.savedTitleNode.isHidden = true } if !self.files.trending.isEmpty { if self.files.isSearch { @@ -545,14 +647,14 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } else { self.trendingTitleNode.isHidden = false if hasContent { - verticalOffset += 15.0 + verticalOffset += 16.0 } let leftInset: CGFloat = 10.0 let trendingTitleSize = self.trendingTitleNode.updateLayout(CGSize(width: drawableSize.width - leftInset * 2.0, height: 100.0)) self.trendingTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: trendingTitleSize) verticalOffset += trendingTitleSize.height + 5.0 } - commitFilesSpans(files: self.files.trending, isTrending: true) + commitFileGrid(files: self.files.trending, isTrending: true) } else { self.trendingTitleNode.isHidden = true } diff --git a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift index 49b617b275..9731d96385 100644 --- a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift @@ -45,7 +45,7 @@ final class PaneSearchContainerNode: ASDisplayNode { return self.contentNode.ready } - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<[MultiplexedVideoNodeFile]?>, cancel: @escaping () -> Void) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise, cancel: @escaping () -> Void) { self.context = context self.mode = mode self.controllerInteraction = controllerInteraction diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index f71a105a68..f1de6e484f 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -201,6 +201,10 @@ private final class VisualMediaItemNode: ASDisplayNode { self.videoLayerFrameManager?.start() } } else { + if let sampleBufferLayer = self.sampleBufferLayer { + sampleBufferLayer.layer.removeFromSuperlayer() + self.sampleBufferLayer = nil + } self.videoLayerFrameManager = nil } @@ -571,8 +575,8 @@ private enum ItemsLayout { return (i, j - 1) } } + return (i, self.frames.count - 1) } - return (i, self.frames.count - 1) } return (0, -1) } @@ -873,10 +877,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro itemsLayout = current } else { switch self.contentType { - case .photoOrVideo: + case .photoOrVideo, .gifs: itemsLayout = .grid(ItemsLayout.Grid(containerWidth: availableWidth, itemCount: self.mediaItems.count, bottomInset: bottomInset)) - case .gifs: - itemsLayout = .balanced(ItemsLayout.Balanced(containerWidth: availableWidth, items: self.mediaItems, bottomInset: bottomInset)) + /*case .gifs: + itemsLayout = .balanced(ItemsLayout.Balanced(containerWidth: availableWidth, items: self.mediaItems, bottomInset: bottomInset))*/ } self.itemsLayout = itemsLayout } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index 9c91f0e88b..45c0dd80bd 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift @@ -216,7 +216,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { totalRawTabSize += paneNodeSize.width } - let minSpacing: CGFloat = 10.0 + let minSpacing: CGFloat = 26.0 if tabSizes.count <= 1 { for i in 0 ..< tabSizes.count { let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] diff --git a/submodules/TelegramUI/Sources/SampleBufferPool.swift b/submodules/TelegramUI/Sources/SampleBufferPool.swift index d0c40a1e12..1e67917933 100644 --- a/submodules/TelegramUI/Sources/SampleBufferPool.swift +++ b/submodules/TelegramUI/Sources/SampleBufferPool.swift @@ -54,11 +54,14 @@ func takeSampleBufferLayer() -> SampleBufferLayer { Queue.mainQueue().async { layer.flushAndRemoveImage() layer.setAffineTransform(CGAffineTransform.identity) + #if targetEnvironment(simulator) + #else let _ = pool.modify { list in var list = list list.append(layer) return list } + #endif } }) } diff --git a/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift b/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift index 6c2756ec97..baa1eb45d1 100644 --- a/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift +++ b/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift @@ -6,32 +6,55 @@ import Postbox import SwiftSignalKit import Display import PhotoResources +import TelegramPresentationData +import AsyncDisplayKit private final class SoftwareVideoThumbnailLayerNullAction: NSObject, CAAction { @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { } } -final class SoftwareVideoThumbnailLayer: CALayer { +final class SoftwareVideoThumbnailNode: ASDisplayNode { + private let usePlaceholder: Bool + private var placeholder: MultiplexedVideoPlaceholderNode? + private var theme: PresentationTheme? + private var asolutePosition: (CGRect, CGSize)? + var disposable = MetaDisposable() var ready: (() -> Void)? { didSet { - if self.contents != nil { + if self.layer.contents != nil { self.ready?() } } } - init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool) { + init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool, usePlaceholder: Bool = false, existingPlaceholder: MultiplexedVideoPlaceholderNode? = nil) { + self.usePlaceholder = usePlaceholder + if usePlaceholder { + self.placeholder = existingPlaceholder + } else { + self.placeholder = nil + } + super.init() - self.backgroundColor = UIColor.clear.cgColor - self.contentsGravity = .resizeAspectFill - self.masksToBounds = true + if !usePlaceholder { + self.isLayerBacked = true + } + + if let placeholder = self.placeholder { + self.addSubnode(placeholder) + } + + self.backgroundColor = UIColor.clear + self.layer.contentsGravity = .resizeAspectFill + self.layer.masksToBounds = true if let dimensions = fileReference.media.dimensions { - self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference, synchronousLoad: synchronousLoad)).start(next: { [weak self] transform in + self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference, synchronousLoad: synchronousLoad, nilForEmptyResult: true) + |> deliverOnMainQueue).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) @@ -40,9 +63,31 @@ final class SoftwareVideoThumbnailLayer: CALayer { Queue.mainQueue().async { if let strongSelf = self { strongSelf.contents = image.cgImage + if let placeholder = strongSelf.placeholder { + strongSelf.placeholder = placeholder + placeholder.removeFromSupernode() + } strongSelf.ready?() } } + } else { + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if strongSelf.usePlaceholder && strongSelf.placeholder == nil { + let placeholder = MultiplexedVideoPlaceholderNode() + strongSelf.placeholder = placeholder + strongSelf.addSubnode(placeholder) + placeholder.frame = strongSelf.bounds + if let theme = strongSelf.theme { + placeholder.update(size: strongSelf.bounds.size, theme: theme) + } + if let (absoluteRect, containerSize) = strongSelf.asolutePosition { + placeholder.updateAbsoluteRect(absoluteRect, within: containerSize) + } + } + } } })) } @@ -56,7 +101,24 @@ final class SoftwareVideoThumbnailLayer: CALayer { self.disposable.dispose() } - override func action(forKey event: String) -> CAAction? { - return SoftwareVideoThumbnailLayerNullAction() + func update(theme: PresentationTheme, size: CGSize) { + if self.usePlaceholder { + self.theme = theme + } + if let placeholder = self.placeholder { + placeholder.frame = CGRect(origin: CGPoint(), size: size) + placeholder.update(size: size, theme: theme) + } } + + func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + self.asolutePosition = (absoluteRect, containerSize) + if let placeholder = self.placeholder { + placeholder.updateAbsoluteRect(absoluteRect, within: containerSize) + } + } + + /*override func action(forKey event: String) -> CAAction? { + return SoftwareVideoThumbnailLayerNullAction() + }*/ }