From c4dd56b59601619e99a86fe0515e4f472b1734e6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 26 May 2020 03:53:24 +0400 Subject: [PATCH] GIF-related fixes part 2 --- .../FFMpegBinding/FFMpegAVCodecContext.h | 10 +- .../Public/FFMpegBinding/FFMpegAVFrame.h | 1 + .../Sources/FFMpegAVCodecContext.m | 15 +- .../FFMpegBinding/Sources/FFMpegAVFrame.m | 4 + .../Sources/FFMpegAudioFrameDecoder.swift | 11 +- .../FFMpegMediaVideoFrameDecoder.swift | 79 ++++- .../Sources/SoftwareVideoSource.swift | 55 ++-- .../MessageHistoryHoleIndexTable.swift | 17 +- .../Sources/MessageHistoryMetadataTable.swift | 35 +++ .../Postbox/Sources/SeedConfiguration.swift | 4 +- .../StandaloneAccountTransaction.swift | 11 +- .../Sources/RequestChatContextResults.swift | 12 +- .../Resources/PresentationResourceKey.swift | 2 + .../Resources/PresentationResourcesChat.swift | 22 ++ .../Media/GifsTabIcon.imageset/Contents.json | 20 +- .../StickerKeyboardGifIcon@2x.png | Bin 1264 -> 0 bytes .../StickerKeyboardGifIcon@3x.png | Bin 1796 -> 0 bytes .../GifsTabIcon.imageset/ic_input_gifs.pdf | Bin 0 -> 4516 bytes .../RecentTabIcon.imageset/Contents.json | 20 +- .../StickerKeyboardRecentTab@2x.png | Bin 696 -> 0 bytes .../StickerKeyboardRecentTab@3x.png | Bin 1082 -> 0 bytes .../ic_input_recent.pdf} | Bin 4092 -> 4065 bytes .../Media/StickersMode.imageset/Contents.json | 12 + .../ic_input_stickers.pdf | Bin 0 -> 4625 bytes .../Media/TrendingGifs.imageset/Contents.json | 12 + .../ic_input_trending.pdf | Bin 0 -> 4517 bytes .../Media/TrendingIcon.imageset/Contents.json | 10 +- .../ic_input_addstickers.pdf | Bin 0 -> 4081 bytes .../TelegramUI/Sources/ChatController.swift | 33 +- .../Sources/ChatControllerInteraction.swift | 6 +- .../Sources/ChatMediaInputGifPane.swift | 165 +++++++--- .../ChatMediaInputMetaSectionItemNode.swift | 95 +++++- .../Sources/ChatMediaInputNode.swift | 211 ++++++++----- .../Sources/ChatMediaInputPanelEntries.swift | 184 ++++++----- .../ChatMessageInteractiveMediaNode.swift | 4 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../Sources/DrawingStickersScreen.swift | 6 +- .../Sources/FeaturedStickersScreen.swift | 4 + .../Sources/GifPaneSearchContentNode.swift | 73 ++--- .../Sources/MultiplexedVideoNode.swift | 297 ++++-------------- .../Sources/OverlayPlayerControllerNode.swift | 2 + .../Sources/PaneSearchContainerNode.swift | 4 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 2 + .../PeerMediaCollectionController.swift | 2 + .../Sources/SharedAccountContext.swift | 4 +- .../SoftwareVideoLayerFrameManager.swift | 11 +- .../Sources/WebSearchController.swift | 4 +- 47 files changed, 893 insertions(+), 568 deletions(-) delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@2x.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@3x.png create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/ic_input_gifs.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@2x.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@3x.png rename submodules/TelegramUI/Images.xcassets/Chat/Input/Media/{TrendingIcon.imageset/ic_addstickers.pdf => RecentTabIcon.imageset/ic_input_recent.pdf} (78%) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/ic_input_stickers.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/ic_input_trending.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_input_addstickers.pdf diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodecContext.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodecContext.h index 92a2c78481..edc65d22d4 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodecContext.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVCodecContext.h @@ -4,6 +4,13 @@ NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSUInteger, FFMpegAVCodecContextReceiveResult) +{ + FFMpegAVCodecContextReceiveResultError, + FFMpegAVCodecContextReceiveResultNotEnoughData, + FFMpegAVCodecContextReceiveResultSuccess, +}; + @class FFMpegAVCodec; @class FFMpegAVFrame; @@ -17,7 +24,8 @@ NS_ASSUME_NONNULL_BEGIN - (FFMpegAVSampleFormat)sampleFormat; - (bool)open; -- (bool)receiveIntoFrame:(FFMpegAVFrame *)frame; +- (bool)sendEnd; +- (FFMpegAVCodecContextReceiveResult)receiveIntoFrame:(FFMpegAVFrame *)frame; - (void)flushBuffers; @end diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h index e1145e3cba..752741faa5 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h @@ -14,6 +14,7 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFrameColorRange) { @property (nonatomic, readonly) uint8_t **data; @property (nonatomic, readonly) int *lineSize; @property (nonatomic, readonly) int64_t pts; +@property (nonatomic, readonly) int64_t duration; @property (nonatomic, readonly) FFMpegAVFrameColorRange colorRange; - (instancetype)init; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m index 5f63c84f80..f41b1a9517 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m @@ -50,11 +50,22 @@ return result >= 0; } -- (bool)receiveIntoFrame:(FFMpegAVFrame *)frame { - int status = avcodec_receive_frame(_impl, (AVFrame *)[frame impl]); +- (bool)sendEnd { + int status = avcodec_send_packet(_impl, nil); return status == 0; } +- (FFMpegAVCodecContextReceiveResult)receiveIntoFrame:(FFMpegAVFrame *)frame { + int status = avcodec_receive_frame(_impl, (AVFrame *)[frame impl]); + if (status == 0) { + return FFMpegAVCodecContextReceiveResultSuccess; + } else if (status == -35) { + return FFMpegAVCodecContextReceiveResultNotEnoughData; + } else { + return FFMpegAVCodecContextReceiveResultError; + } +} + - (void)flushBuffers { avcodec_flush_buffers(_impl); } diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m index 2598869967..e905c08431 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m @@ -44,6 +44,10 @@ return _impl->pts; } +- (int64_t)duration { + return _impl->pkt_duration; +} + - (FFMpegAVFrameColorRange)colorRange { switch (_impl->color_range) { case AVCOL_RANGE_MPEG: diff --git a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift index 580d15a5c4..8324cb4d8e 100644 --- a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift @@ -21,9 +21,14 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? { let status = frame.packet.send(toDecoder: self.codecContext) if status == 0 { - while self.codecContext.receive(into: self.audioFrame) { - if let convertedFrame = convertAudioFrame(self.audioFrame, pts: frame.pts, duration: frame.duration) { - self.delayedFrames.append(convertedFrame) + while true { + let result = self.codecContext.receive(into: self.audioFrame) + if case .success = result { + if let convertedFrame = convertAudioFrame(self.audioFrame, pts: frame.pts, duration: frame.duration) { + self.delayedFrames.append(convertedFrame) + } + } else { + break } } diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index 1c87095692..a6be0700ae 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -17,11 +17,20 @@ private let deviceColorSpace: CGColorSpace = { }() public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { + public enum ReceiveResult { + case error + case moreDataNeeded + case result(MediaTrackFrame) + } + private let codecContext: FFMpegAVCodecContext private let videoFrame: FFMpegAVFrame private var resetDecoderOnNextFrame = true + private var defaultDuration: CMTime? + private var defaultTimescale: CMTimeScale? + private var pixelBufferPool: CVPixelBufferPool? private var delayedFrames: [MediaTrackFrame] = [] @@ -60,10 +69,51 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { return self.decode(frame: frame, ptsOffset: nil) } + public func sendToDecoder(frame: MediaTrackDecodableFrame) -> Bool { + self.defaultDuration = frame.duration + self.defaultTimescale = frame.pts.timescale + + let status = frame.packet.send(toDecoder: self.codecContext) + return status == 0 + } + + public func sendEndToDecoder() -> Bool { + return self.codecContext.sendEnd() + } + + public func receiveFromDecoder(ptsOffset: CMTime?) -> ReceiveResult { + guard let defaultTimescale = self.defaultTimescale, let defaultDuration = self.defaultDuration else { + return .error + } + + let receiveResult = self.codecContext.receive(into: self.videoFrame) + switch receiveResult { + case .success: + var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale) + if let ptsOffset = ptsOffset { + pts = CMTimeAdd(pts, ptsOffset) + } + if let convertedFrame = convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: self.videoFrame.duration > 0 ? CMTimeMake(value: self.videoFrame.duration, timescale: defaultTimescale) : defaultDuration) { + return .result(convertedFrame) + } else { + return .error + } + case .notEnoughData: + return .moreDataNeeded + case .error: + return .error + @unknown default: + return .error + } + } + public func decode(frame: MediaTrackDecodableFrame, ptsOffset: CMTime?) -> MediaTrackFrame? { let status = frame.packet.send(toDecoder: self.codecContext) if status == 0 { - if self.codecContext.receive(into: self.videoFrame) { + self.defaultDuration = frame.duration + self.defaultTimescale = frame.pts.timescale + + if self.codecContext.receive(into: self.videoFrame) == .success { var pts = CMTimeMake(value: self.videoFrame.pts, timescale: frame.pts.timescale) if let ptsOffset = ptsOffset { pts = CMTimeAdd(pts, ptsOffset) @@ -75,10 +125,35 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { return nil } + public func receiveRemainingFrames(ptsOffset: CMTime?) -> [MediaTrackFrame] { + guard let defaultTimescale = self.defaultTimescale, let defaultDuration = self.defaultDuration else { + return [] + } + + var result: [MediaTrackFrame] = [] + result.append(contentsOf: self.delayedFrames) + self.delayedFrames.removeAll() + + while true { + if case .success = self.codecContext.receive(into: self.videoFrame) { + var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale) + if let ptsOffset = ptsOffset { + pts = CMTimeAdd(pts, ptsOffset) + } + if let convertedFrame = convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: self.videoFrame.duration > 0 ? CMTimeMake(value: self.videoFrame.duration, timescale: defaultTimescale) : defaultDuration) { + result.append(convertedFrame) + } + } else { + break + } + } + return result + } + public func render(frame: MediaTrackDecodableFrame) -> UIImage? { let status = frame.packet.send(toDecoder: self.codecContext) if status == 0 { - if self.codecContext.receive(into: self.videoFrame) { + if case .success = self.codecContext.receive(into: self.videoFrame) { return convertVideoFrameToImage(self.videoFrame) } } diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index 453ce05652..1e252012a7 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -54,6 +54,9 @@ public final class SoftwareVideoSource { fileprivate let fd: Int32? fileprivate let size: Int32 + private var enqueuedFrames: [(MediaTrackFrame, CGFloat, CGFloat, Bool)] = [] + private var hasReadToEnd: Bool = false + public init(path: String) { let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals @@ -178,7 +181,7 @@ public final class SoftwareVideoSource { } else { if let avFormatContext = self.avFormatContext, let videoStream = self.videoStream { endOfStream = true - avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true) + break } else { endOfStream = true break @@ -187,30 +190,44 @@ public final class SoftwareVideoSource { } } - if endOfStream { - if let videoStream = self.videoStream { - videoStream.decoder.reset() - } - } - return (frames.first, endOfStream) } public func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) { - if let videoStream = self.videoStream { - let (decodableFrame, loop) = self.readDecodableFrame() - if let decodableFrame = decodableFrame { - var ptsOffset: CMTime? - if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 { - ptsOffset = maxPts - } - return (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) - } else { - return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) - } - } else { + guard let videoStream = self.videoStream, let avFormatContext = self.avFormatContext else { return (nil, 0.0, 1.0, false) } + + if !self.enqueuedFrames.isEmpty { + let value = self.enqueuedFrames.removeFirst() + return (value.0, value.1, value.2, value.3) + } + + let (decodableFrame, loop) = self.readDecodableFrame() + var result: (MediaTrackFrame?, CGFloat, CGFloat, Bool) + if let decodableFrame = decodableFrame { + var ptsOffset: CMTime? + if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 { + ptsOffset = maxPts + } + result = (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) + } else { + result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) + } + if loop { + let _ = videoStream.decoder.sendEndToDecoder() + let remainingFrames = videoStream.decoder.receiveRemainingFrames(ptsOffset: maxPts) + for i in 0 ..< remainingFrames.count { + self.enqueuedFrames.append((remainingFrames[i], CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), i == remainingFrames.count - 1)) + } + videoStream.decoder.reset() + avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true) + + if result.0 == nil && !self.enqueuedFrames.isEmpty { + result = self.enqueuedFrames.removeFirst() + } + } + return result } public func readImage() -> (UIImage?, CGFloat, CGFloat, Bool) { diff --git a/submodules/Postbox/Sources/MessageHistoryHoleIndexTable.swift b/submodules/Postbox/Sources/MessageHistoryHoleIndexTable.swift index 8851e0ca48..d997c179b9 100644 --- a/submodules/Postbox/Sources/MessageHistoryHoleIndexTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryHoleIndexTable.swift @@ -130,11 +130,26 @@ final class MessageHistoryHoleIndexTable: Table { if !self.metadataTable.isInitialized(peerId) { self.metadataTable.setInitialized(peerId) if let tagsByNamespace = self.seedConfiguration.messageHoles[peerId.namespace] { - for (namespace, _) in tagsByNamespace { + for (namespace, tags) in tagsByNamespace { + for tag in tags { + self.metadataTable.setPeerTagInitialized(peerId: peerId, tag: tag) + } var operations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:] self.add(peerId: peerId, namespace: namespace, space: .everywhere, range: 1 ... (Int32.max - 1), operations: &operations) } } + } else { + if let tagsByNamespace = self.seedConfiguration.upgradedMessageHoles[peerId.namespace] { + for (namespace, tags) in tagsByNamespace { + for tag in tags { + if !self.metadataTable.isPeerTagInitialized(peerId: peerId, tag: tag) { + self.metadataTable.setPeerTagInitialized(peerId: peerId, tag: tag) + var operations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:] + self.add(peerId: peerId, namespace: namespace, space: .tag(tag), range: 1 ... (Int32.max - 1), operations: &operations) + } + } + } + } } } diff --git a/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift b/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift index 34d3d3ca10..5ed99c8cca 100644 --- a/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift @@ -12,6 +12,7 @@ private enum MetadataPrefix: Int8 { case PeerHistoryInitialized = 9 case ShouldReindexUnreadCountsState = 10 case TotalUnreadCountStates = 11 + case PeerHistoryTagInitialized = 12 } public struct ChatListTotalUnreadCounters: PostboxCoding, Equatable { @@ -51,6 +52,7 @@ final class MessageHistoryMetadataTable: Table { private var initializedChatList = Set() private var initializedHistoryPeerIds = Set() + private var initializedHistoryPeerIdTags: [PeerId: Set] = [:] private var initializedGroupFeedIndexIds = Set() private var peerNextMessageIdByNamespace: [PeerId: [MessageId.Namespace: MessageId.Id]] = [:] @@ -74,6 +76,14 @@ final class MessageHistoryMetadataTable: Table { return self.sharedPeerHistoryInitializedKey } + private func peerHistoryInitializedTagKey(id: PeerId, tag: UInt32) -> ValueBoxKey { + let key = ValueBoxKey(length: 8 + 1 + 4) + key.setInt64(0, value: id.toInt64()) + key.setInt8(8, value: MetadataPrefix.PeerHistoryTagInitialized.rawValue) + key.setUInt32(8 + 1, value: tag) + return key + } + private func groupFeedIndexInitializedKey(_ id: PeerGroupId) -> ValueBoxKey { self.sharedGroupFeedIndexInitializedKey.setInt32(0, value: id.rawValue) self.sharedGroupFeedIndexInitializedKey.setInt8(4, value: MetadataPrefix.GroupFeedIndexInitialized.rawValue) @@ -201,6 +211,31 @@ final class MessageHistoryMetadataTable: Table { } } + func setPeerTagInitialized(peerId: PeerId, tag: MessageTags) { + if self.initializedHistoryPeerIdTags[peerId] == nil { + self.initializedHistoryPeerIdTags[peerId] = Set() + } + initializedHistoryPeerIdTags[peerId]!.insert(tag) + self.sharedBuffer.reset() + self.valueBox.set(self.table, key: self.peerHistoryInitializedTagKey(id: peerId, tag: tag.rawValue), value: self.sharedBuffer) + } + + func isPeerTagInitialized(peerId: PeerId, tag: MessageTags) -> Bool { + if let currentTags = self.initializedHistoryPeerIdTags[peerId], currentTags.contains(tag) { + return true + } else { + if self.valueBox.exists(self.table, key: self.peerHistoryInitializedTagKey(id: peerId, tag: tag.rawValue)) { + if self.initializedHistoryPeerIdTags[peerId] == nil { + self.initializedHistoryPeerIdTags[peerId] = Set() + } + initializedHistoryPeerIdTags[peerId]!.insert(tag) + return true + } else { + return false + } + } + } + func setGroupFeedIndexInitialized(_ groupId: PeerGroupId) { self.initializedGroupFeedIndexIds.insert(groupId) self.sharedBuffer.reset() diff --git a/submodules/Postbox/Sources/SeedConfiguration.swift b/submodules/Postbox/Sources/SeedConfiguration.swift index 751c4d858b..de9aa9a185 100644 --- a/submodules/Postbox/Sources/SeedConfiguration.swift +++ b/submodules/Postbox/Sources/SeedConfiguration.swift @@ -59,6 +59,7 @@ public final class SeedConfiguration { public let globalMessageIdsPeerIdNamespaces: Set public let initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?) public let messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set]] + public let upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set]] public let messageTagsWithSummary: MessageTags public let existingGlobalMessageTags: GlobalMessageTags public let peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace] @@ -70,10 +71,11 @@ public final class SeedConfiguration { public let globalNotificationSettingsPreferencesKey: ValueBoxKey public let defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings - public init(globalMessageIdsPeerIdNamespaces: Set, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer, Bool) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState], chatMessagesNamespaces: Set, globalNotificationSettingsPreferencesKey: ValueBoxKey, defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings) { + public init(globalMessageIdsPeerIdNamespaces: Set, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set]], upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer, Bool) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState], chatMessagesNamespaces: Set, globalNotificationSettingsPreferencesKey: ValueBoxKey, defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings) { self.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces self.initializeChatListWithHole = initializeChatListWithHole self.messageHoles = messageHoles + self.upgradedMessageHoles = upgradedMessageHoles self.messageTagsWithSummary = messageTagsWithSummary self.existingGlobalMessageTags = existingGlobalMessageTags self.peerNamespacesRequiringMessageTextIndex = peerNamespacesRequiringMessageTextIndex diff --git a/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift b/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift index 3072e46c17..a467a41d80 100644 --- a/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift +++ b/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift @@ -13,12 +13,21 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { ] } + // To avoid upgrading the database, **new** tags can be added here + // Uninitialized peers will fill the info using messageHoles + var upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set]] = [:] + for peerNamespace in peerIdNamespacesWithInitialCloudMessageHoles { + upgradedMessageHoles[peerNamespace] = [ + Namespaces.Message.Cloud: Set(MessageTags.gif) + ] + } + var globalMessageIdsPeerIdNamespaces = Set() for peerIdNamespace in [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup] { globalMessageIdsPeerIdNamespaces.insert(GlobalMessageIdsNamespace(peerIdNamespace: peerIdNamespace, messageIdNamespace: Namespaces.Message.Cloud)) } - return SeedConfiguration(globalMessageIdsPeerIdNamespaces: globalMessageIdsPeerIdNamespaces, initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1))), messageHoles: messageHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: MessageTags.unseenPersonalMessage, existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer, isContact in + return SeedConfiguration(globalMessageIdsPeerIdNamespaces: globalMessageIdsPeerIdNamespaces, initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1))), messageHoles: messageHoles, upgradedMessageHoles: upgradedMessageHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: MessageTags.unseenPersonalMessage, existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer, isContact in if let peer = peer as? TelegramUser { if peer.botInfo != nil { return .bot diff --git a/submodules/TelegramCore/Sources/RequestChatContextResults.swift b/submodules/TelegramCore/Sources/RequestChatContextResults.swift index 5008d2dbd6..680717dc13 100644 --- a/submodules/TelegramCore/Sources/RequestChatContextResults.swift +++ b/submodules/TelegramCore/Sources/RequestChatContextResults.swift @@ -41,7 +41,7 @@ private struct RequestData: Codable { 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 { +public func requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false) -> Signal { return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) { return (bot, peer) @@ -92,7 +92,9 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P flags |= (1 << 0) geoPoint = Api.InputGeoPoint.inputGeoPoint(lat: latitude, long: longitude) } - return account.network.request(Api.functions.messages.getInlineBotResults(flags: flags, bot: inputBot, peer: inputPeer, geoPoint: geoPoint, query: query, offset: offset)) + + + var signal: Signal = account.network.request(Api.functions.messages.getInlineBotResults(flags: flags, bot: inputBot, peer: inputPeer, geoPoint: geoPoint, query: query, offset: offset)) |> map { result -> ChatContextResultCollection? in return ChatContextResultCollection(apiResults: result, botId: bot.id, peerId: peerId, query: query, geoPoint: location) } @@ -122,6 +124,12 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P } |> castError(RequestChatContextResultsError.self) } + + if incompleteResults { + signal = .single(nil) |> then(signal) + } + + return signal } |> castError(RequestChatContextResultsError.self) |> switchToLatest diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index d6e87f5af4..9f9013777e 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -130,6 +130,8 @@ public enum PresentationResourceKey: Int32 { case chatInputMediaPanelAddedPackButtonImage case chatInputMediaPanelGridSetupImage case chatInputMediaPanelGridDismissImage + case chatInputMediaPanelTrendingGifsIcon + case chatInputMediaPanelStickersModeIcon case chatInputButtonPanelButtonImage case chatInputButtonPanelButtonHighlightedImage diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 2cf976c8f3..51f16a2eb5 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -214,6 +214,28 @@ public struct PresentationResourcesChat { }) } + public static func chatInputMediaPanelStickersModeIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputMediaPanelStickersModeIcon.rawValue, { theme in + return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/StickersMode"), color: theme.chat.inputMediaPanel.panelIconColor) { + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + } + }) + }) + } + + public static func chatInputMediaPanelTrendingGifsIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputMediaPanelTrendingGifsIcon.rawValue, { theme in + return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/TrendingGifs"), color: theme.chat.inputMediaPanel.panelIconColor) { + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + } + }) + }) + } + public static func chatInputMediaPanelRecentStickersIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputMediaPanelRecentStickersIconImage.rawValue, { theme in return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/Contents.json index 32243bea94..b741dea23c 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/Contents.json @@ -1,22 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "StickerKeyboardGifIcon@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "StickerKeyboardGifIcon@3x.png", - "scale" : "3x" + "filename" : "ic_input_gifs.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@2x.png deleted file mode 100644 index 382c35e0335b2625cac33a8ebc30ea1b6fbb2cd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1264 zcmV0lI$f>AIEMnT8Snf#rT0MuCnkn~6tu07lK70#uzZQH&p913R_FUc~US$AiSzir#L zZQI8CYLornX2(eeRnsP_y@eoddhVBotJk?-Oi<`gtZz4Gsy zKuQ;KtS;1H&6GzH4a~t@tijrXAo`Qf^jyh}IbbT3ePwiSfPN^ezG zwmd7D9L&L7tijqC@}@&qZpjMu0a<3RCl*&3LVhl#JTX(bSPL1D#brYmbmqi`NpXoM z=CX;SMq^y)H2Klo8 z|35u!z0d(&UQ34_vmS*Nw97?V45cFI^kDcPv}}x|!7akMR zH}>z|ET(}i(0Ind&$F@r1Qkpogd({lE8?lSI|Jj8oIDwjdw`^Y%F>G;-jik<5+yWmCzfiqAzPg zt!Aa)VaLtZ8kTyk>6xxx#CrmIuteB5I=W`*^eo8*!Nyo#25e04xQwr5pv3KG6Sa5* zG}@@JLhd7ytI5sQ(QmiMJw&qV-T8l-^{Qbr>NnD6e2HhIkDl#@b=U?e#~K?nVSmL5 z(>Q?5X+C7NUa1~RCAk87Ch_~8D*(3=G2^xF|BH@UQSBs9ZH4&f9QS?u_U(_?Blais zydTB%`zaFMlZi(HBa7qrGB|(b*U1tVJ^vOi1mN^JambJ%CnOOY9$}WafZbPy#vN2_ zH&Xo_57=I8*&n*kYiin`L6&_MSr-G}Q6!{D7CQpN!s2O)X4W5~_XjXQhGYy#nXqTz9T9pmiG?dT;jmnKh-2ZMhOpmZ&sW8` zcHNOVVKosQl7E->nY4)HABbVH?i%_W=*h4EkaIZw~59>Ids6VB#P!sAeBoX?Y#QW&5 z2zv_;{0^%Sx`Zg;E+i@T-b z4xHEA)sf)bTH1)%w>gBTIwj~xKW8v7hu4$l&8MUp> zrA>H~S87MwH47tvVE_c~-wnELjx%x^_owbIfp0G!VgO+AVB+0j(_+LEwGgJozmku{ zrGB0jMn4mX`=I}C74o2MYaf7Gz?L=w)6}$sX`qMe;Zm6hxp1lGHC%crKCE%ZhY>;u ay{!OWv{!a9ZlW>(0000|6fdS;8GYcjm4$s!xR>e!>I!p z8m9>K0gT4vNh+5DnoKTO0=@+`n|*-+b|SM}FbgV`%>xG-3kJw64v%*J5|sszJbgFzmJ zCD8-y0G8znxERGi1ELLVX23aRWpp-T5U?Oz`OB6O*9aXOI7Ku|h!9Dcl0IMr0MMs1 zxFEpXf})(lGKpX$IvTSCIWu6ena%>%z#fG2s=zI;35G^WFQohNU6+X z0H!jKJ`>R?A(G&i0Td)R$v7A+xCo3$mcW!eq)$WGfv@G7)*?&w*JCXPGk?!jJ z8OC7X$*^WC<%p;|)fUY1(r&6*)v0SFzazfG2F7j+nU+D$zdbwDHW3PE6Td`0_ zvP*73|N0);A#OY!vskfqA+>cbiyvSkqWr|JnW=%B&Sz^LQ?XGqkbdZH5w;}Ve9%+SU=-3fSC0jK1vj>EWohmbH{ zxqs;X$C8w?+tm>RcYR{+1eV^8D-GNQcdsXgqtV{A(Y-lDZEu6uM&0?Ba>EYqGhaR9 z`aZPA*e7)=Ec03R4^Q-LNv3 z|C<_pJ1i*c>F&;qgf`FMMppPX`i9(N1e7CD-6q{*4d1XFXCJPPdK0rXkME!UbJSVs zsW57V6HmXBs{Ot`fg9aoFTic3@_VmD4AY(5*&&)vIgQ&iWkV(j6zhJ}`@kgL}B4G${5ENG;c!tss~r)eta@Y^ZwQUAnS_mXDtg$=aAadz zDZ6dB1QU2=om9hqPSs^~FLWM6)Yp?TF>+9P*W%Ei;`0%Eug0#`3g<`~3)5nAE()vD zg^~B7KGp3;P3v|ATl&;Q`BCw!AGBAzQo-F&5Iv5bOyv~(KKaKD+hZyN1Wu=Xbvgb! zv;&>2KFlsqDDER}!^J8D3|>ogeWLD%iVvz4?3@?9P&3?f(S5BS=`>V&pfw*l?DFz+ zu!eE+uU$!o#9KbTkhK15ITPK{q_j~E*RRFu8^rd`4tO`jf7327uBBl4VW zCUdrj6L0*y#H`EC|32y5e$`00c?x}Iky5e6j-P_!kTEvzSd`y0d%d}#u5Q!!um&T3^jY}KTsqm7-{iM!SNI diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/ic_input_gifs.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/ic_input_gifs.pdf new file mode 100644 index 0000000000000000000000000000000000000000..50f89933d7b8f83c423815c708bdc519552ee577 GIT binary patch literal 4516 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/Contents.json index 715abcaaf5..8f506681e9 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/Contents.json @@ -1,22 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "StickerKeyboardRecentTab@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "StickerKeyboardRecentTab@3x.png", - "scale" : "3x" + "filename" : "ic_input_recent.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@2x.png deleted file mode 100644 index 371aa0aee96515919a43cc7a280d4f52ec0bb30f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmV;p0!RIcP)`Lkk|~C; z3`!E;-R{cmzyne4%|6+ko0**_XU?2Cb5?YFd3~!8>V$;QBMcZlLW_T^Ji5pli&_&> z!q+cI32T0)_9!Atgg#-_s=N12Ux$*3QxqVy2 zHVC^b3q7%*m=OJeCnCiB%t`XGkA`WnZNjHm*pbjB1ORWq97mO#eYH((;jviEnJ^@T z8TtvC^Q`(}-@Z<NTfXu~k8?k>!r_eIX*^0E zVJ6nl&Mg{?0JQ|fqN0}~kTvo+w>xoW%fNyYh6il#t$1bmSqkomXj}!j5``;40-1>3 zoy^ijkwFbXzLCN;0=b4FBP(SgxG$25&?3R1NKX2&U{MHkVLmAs=j*Bk2VCNb+&HjM za3FH;8bUTui01sYRf3swu5bXfW}JG^CcsM9b82ZKim@MK^)s!9aP4 zi*5qtp~eOPF1pF^kOqMH77uQCiwAGw;by3bhqq~Q!`rl48}Yc|jd*v};x5zfb2=`< zb*Vhhiy~ZzOVJZkBMQ_2HNu<#0000HW~n_GXXv!uK)l4Z|$2oJP_yn-%o-5 zaw!S&3ufTd3y9CDWYo#q@ZjHn!@lRNiI=k0z-2GWj=$rYoJq!#?H$7b(Ln;{G%xLXg zvGHf4KmYA*D>v;a%YU?wqx#6KC#OCwXkWME;8bn7_2=a|*qH0)Uw8iB8(<;Sd-SOK z#|RT4zKc)qs7!KlG%9o!pXim|GR?;-Rd3GO&dpCgwLNNG^kY%Tx{&OzRcoU(rymhN z%4E6xqKN#KTXXaJ?Ov8zzrD76<4uM3FZS<~3}i0KyB+@ceC?N$f|FlXFF5;bNupgf z|B07JJXQ2tj^6N2|4=u_Ceiv-u=l|=e;!PqP_H^8_0+SO_osGeH?8S@f9u=1xdJ|^ zE@l#;ER#1pbv&4}Ev5Udn}KlCm$f%eOwRojHs^pu&XYC9lbP2PKM!~pVHt6A67x1u zZGFeP8g=%^9{U?aO9qPntf*d-C+`&{-t~+hT{lCuSohDPCvX^rmwiSQny<}(3 z{nuULUxi+-T9?DNWMNrniEo|2g{hCNCe>A_=$7ieXuGtAcZsjwlaGuU*?KHH1T&b; ze>r?e%;mG2f9L_LX7g6d)IHyXWwRf u?J_ODc@eiA)ckYSNJ?ffxE<7xU}bPGEK9ByPDlV|8U{~SKbLh*2~7ZtP7A~U diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_addstickers.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/ic_input_recent.pdf similarity index 78% rename from submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_addstickers.pdf rename to submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/ic_input_recent.pdf index cbe24a9aa03b3c4f5b5a753de93d666a7b5f0d8c..7afd3b024ff75b942e81b38630ce506a286ea4c7 100644 GIT binary patch delta 710 zcmew(|4@EHP`zUwvmwWx-=Z}O))`7XE{>lNEWaUWs+Z?djsMfRZoJmoyQcl-&BO!0 z8@O#4q(e@66|{Hl6f})GvgH3nHT!GTXLSzAelPDdDl_l-+@RZGCAU6g2J8OabDB(c zzb|2rQO^1-RxUAf$|{2^3OCqSsB(LycWCXAUAp$}1Cb+{kN4EnTgYxNoAyx5Aor_$ z2alZT0ZGy4VH;D|wTPK=XjiWfNzV4TGTAUH`R$C228(iUSkC5nxNU*pm7}L8N_z_* zn6+`!S_O-#x7?kw57us2&6c$~fk!iZ_q2(r0#<=D3MzybA7b>D>#c~KXsz{LeiHZI(aUHYL5ZRe#4Hrpu2PyKtrEr4*MGr6%TrSouj=T&4;J3i)h6hOwE^ z<`0aMxmk@36pRffcknjy8!NU1N z<{6uqn@o=9w~IGSF*ZsuGqbQXGd4>#HBU)2N=q^`OG`5~OEOC{HZ-u~vLUEMAy&c8 aj;pvNv8be?C^ZcjU=|i!s;aL3Zd?F6PXB-a delta 783 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json index 3fd562e2a7..52c50ded21 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "ic_addstickers.pdf" + "filename" : "ic_input_addstickers.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_input_addstickers.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_input_addstickers.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4e0a98ec48f973a8017effcdcbf5acf230f32ed0 GIT binary patch literal 4081 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 Void let sendSticker: (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool let sendGif: (FileMediaReference, ASDisplayNode, CGRect) -> Bool + let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void let requestMessageActionUrlAuth: (String, MessageId, Int32) -> Void let activateSwitchInline: (PeerId?, String) -> Void @@ -135,7 +136,7 @@ public final class ChatControllerInteraction { var searchTextHighightState: (String, [MessageIndex])? var seenOneTimeAnimatedMedia = Set() - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, NSAttributedString, TextSelectionAction) -> Void, updateMessageLike: @escaping (MessageId, Bool) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, displayPsa: @escaping (String, ASDisplayNode) -> Void, displayDiceTooltip: @escaping (TelegramMediaDice) -> Void, animateDiceSuccess: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, NSAttributedString, TextSelectionAction) -> Void, updateMessageLike: @escaping (MessageId, Bool) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, displayPsa: @escaping (String, ASDisplayNode) -> Void, displayDiceTooltip: @escaping (TelegramMediaDice) -> Void, animateDiceSuccess: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -149,6 +150,7 @@ public final class ChatControllerInteraction { self.sendMessage = sendMessage self.sendSticker = sendSticker self.sendGif = sendGif + self.sendBotContextResultAsGif = sendBotContextResultAsGif self.requestMessageActionCallback = requestMessageActionCallback self.requestMessageActionUrlAuth = requestMessageActionUrlAuth self.activateSwitchInline = activateSwitchInline @@ -209,7 +211,7 @@ public final class ChatControllerInteraction { static var `default`: ChatControllerInteraction { return ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil }, chatControllerNode: { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift index 138e2e1518..bfbca28bcb 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift @@ -32,7 +32,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void - private let openGifContextMenu: (FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void + private let openGifContextMenu: (MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void private let searchPlaceholderNode: PaneSearchBarPlaceholderNode var visibleSearchPlaceholderNode: PaneSearchBarPlaceholderNode? { @@ -49,14 +49,16 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private let emptyNode: ImmediateTextNode private let disposable = MetaDisposable() - let trendingPromise = Promise<[FileMediaReference]?>(nil) + let trendingPromise = Promise<[MultiplexedVideoNodeFile]?>(nil) private var validLayout: (CGSize, CGFloat, CGFloat, Bool, Bool, DeviceMetrics)? private var didScrollPreviousOffset: CGFloat? private var didScrollPreviousState: ChatMediaInputPaneScrollState? - 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, Bool) -> Void) { + private(set) var mode: ChatMediaInputGifMode = .recent + + 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 self.theme = theme self.strings = strings @@ -72,6 +74,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { self.emptyNode.attributedText = NSAttributedString(string: strings.Gif_NoGifsPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) self.emptyNode.textAlignment = .center self.emptyNode.maximumNumberOfLines = 3 + self.emptyNode.isHidden = true super.init() @@ -116,7 +119,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { self.updateMultiplexedNodeLayout(changedIsExpanded: changedIsExpanded, transition: transition) } - func fileAt(point: CGPoint) -> (FileMediaReference, CGRect, Bool)? { + func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? { if let multiplexedNode = self.multiplexedNode { return multiplexedNode.fileAt(point: point.offsetBy(dx: -multiplexedNode.frame.minX, dy: -multiplexedNode.frame.minY)) } else { @@ -124,6 +127,14 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } } + func setMode(mode: ChatMediaInputGifMode) { + if self.mode == mode { + return + } + self.mode = mode + self.resetMode(synchronous: true) + } + override var isEmpty: Bool { if let files = self.multiplexedNode?.files { return files.trending.isEmpty && files.saved.isEmpty @@ -139,13 +150,23 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } private func updateMultiplexedNodeLayout(changedIsExpanded: Bool, transition: ContainedViewLayoutTransition) { - guard let (size, topInset, bottomInset, isExpanded, _, deviceMetrics) = self.validLayout else { + guard let (size, topInset, bottomInset, _, _, deviceMetrics) = self.validLayout else { return } if let multiplexedNode = self.multiplexedNode { - let previousBounds = multiplexedNode.scrollNode.layer.bounds - multiplexedNode.topInset = topInset + 60.0 + let _ = multiplexedNode.scrollNode.layer.bounds + + let displaySearch: Bool + + switch self.mode { + case .recent: + displaySearch = true + default: + displaySearch = false + } + + multiplexedNode.topInset = topInset + (displaySearch ? 60.0 : 0.0) multiplexedNode.bottomInset = bottomInset if case .tablet = deviceMetrics.type, size.width > 480.0 { @@ -156,11 +177,11 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) - var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size) + /*var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size) if changedIsExpanded { 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.updateFrame(node: multiplexedNode, frame: nodeFrame) @@ -172,8 +193,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { func initializeIfNeeded() { if self.multiplexedNode == nil { - self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, updateActivity: nil) - |> map { items -> [FileMediaReference]? in + 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 } else { @@ -197,47 +218,12 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { self.addSubnode(multiplexedNode) multiplexedNode.scrollNode.addSubnode(self.searchPlaceholderNode) - 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 { - saved = recentGifs.items.map { item in - let file = (item.contents as! RecentMediaItem).media as! TelegramMediaFile - return .savedGif(media: file) - } + multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in + if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect) } else { - saved = [] + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect) } - - return MultiplexedVideoNodeFiles(saved: saved, trending: trending ?? []) - } - self.disposable.set((gifs - |> deliverOnMainQueue).start(next: { [weak self] files in - if let strongSelf = self { - let previousFiles = strongSelf.multiplexedNode?.files - 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) - } - } - })) - - multiplexedNode.fileSelected = { [weak self] fileReference, sourceNode, sourceRect in - let _ = self?.controllerInteraction.sendGif(fileReference, sourceNode, sourceRect) } multiplexedNode.fileContextMenu = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in @@ -273,6 +259,85 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } self.updateMultiplexedNodeLayout(changedIsExpanded: false, transition: .immediate) + + self.resetMode(synchronous: false) } } + + private func resetMode(synchronous: Bool) { + let filesSignal: Signal + 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 + var recentGifs: OrderedItemListView? + if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] { + recentGifs = orderedView as? OrderedItemListView + } + + var saved: [MultiplexedVideoNodeFile] = [] + + if let recentGifs = recentGifs { + saved = recentGifs.items.map { item in + let file = (item.contents as! RecentMediaItem).media as! TelegramMediaFile + return MultiplexedVideoNodeFile(file: .savedGif(media: file), contextResult: nil) + } + } else { + saved = [] + } + + return MultiplexedVideoNodeFiles(saved: saved, trending: trending ?? [], isSearch: false) + } + case .trending: + filesSignal = self.trendingPromise.get() + |> map { trending -> MultiplexedVideoNodeFiles in + return MultiplexedVideoNodeFiles(saved: [], trending: trending ?? [], isSearch: true) + } + 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) + } + } + + var firstTime = true + + self.disposable.set((filesSignal + |> deliverOnMainQueue).start(next: { [weak self] files in + if let strongSelf = self { + //let previousFiles = strongSelf.multiplexedNode?.files + var resetScrollingToOffset: CGFloat? + if firstTime { + firstTime = false + resetScrollingToOffset = 0.0 + } + + let displaySearch: Bool + + switch strongSelf.mode { + case .recent: + displaySearch = true + default: + displaySearch = false + } + + strongSelf.searchPlaceholderNode.isHidden = !displaySearch + + if let (_, topInset, _, _, _, _) = strongSelf.validLayout { + 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 + } + let isEmpty = files.trending.isEmpty && files.saved.isEmpty + strongSelf.emptyNode.isHidden = !isEmpty*/ + } + })) + } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift index c30486cd36..afdd2aac25 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift @@ -8,9 +8,13 @@ import SwiftSignalKit import Postbox import TelegramPresentationData -enum ChatMediaInputMetaSectionItemType { +enum ChatMediaInputMetaSectionItemType: Equatable { case savedStickers case recentStickers + case stickersMode + case savedGifs + case trendingGifs + case gifEmoji(String) } final class ChatMediaInputMetaSectionItem: ListViewItem { @@ -20,7 +24,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { let selectedItem: () -> Void var selectable: Bool { - return true + return false } init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, selected: @escaping () -> Void) { @@ -34,7 +38,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { async { let node = ChatMediaInputMetaSectionItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) - node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction node.setItem(item: self) node.updateTheme(theme: self.theme) @@ -71,7 +75,10 @@ private let verticalOffset: CGFloat = 3.0 + UIScreenPixel final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { private let imageNode: ASImageNode + private let textNodeContainer: ASDisplayNode + private let textNode: ImmediateTextNode private let highlightNode: ASImageNode + private let buttonNode: HighlightTrackingButtonNode var item: ChatMediaInputMetaSectionItem? var currentCollectionId: ItemCollectionId? @@ -87,26 +94,57 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true + self.textNodeContainer = ASDisplayNode() + self.textNodeContainer.isUserInteractionEnabled = false + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + self.textNodeContainer.addSubnode(self.textNode) + self.textNodeContainer.isUserInteractionEnabled = false + 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.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + 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) + } + + override func didLoad() { + super.didLoad() + } + + @objc private func buttonPressed() { + self.item?.selectedItem() } func setItem(item: ChatMediaInputMetaSectionItem) { self.item = item switch item.type { - case .savedStickers: - self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0) - case .recentStickers: - self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) + case .savedStickers: + self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0) + case .recentStickers: + self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) + default: + break } } @@ -117,18 +155,51 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) if let item = self.item { switch item.type { - case .savedStickers: - self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme) - case .recentStickers: - self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme) + case .savedStickers: + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme) + case .recentStickers: + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme) + case .stickersMode: + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelStickersModeIcon(theme) + case .savedGifs: + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme) + case .trendingGifs: + 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) + 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) } } } } func updateIsHighlighted() { - if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction { + guard let inputNodeInteraction = self.inputNodeInteraction else { + return + } + if let currentCollectionId = self.currentCollectionId { self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId + } else if let item = self.item { + var isHighlighted = false + switch item.type { + case .savedGifs: + if case .recent = inputNodeInteraction.highlightedGifMode { + isHighlighted = true + } + case .trendingGifs: + if case .trending = inputNodeInteraction.highlightedGifMode { + isHighlighted = true + } + case let .gifEmoji(emoji): + if case .emojiSearch(emoji) = inputNodeInteraction.highlightedGifMode { + isHighlighted = true + } + default: + break + } + self.highlightNode.isHidden = !isHighlighted } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index c84520421e..931f3b44e3 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -163,7 +163,7 @@ func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemColle return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated) } -func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasGifs: Bool = true, hasUnreadTrending: Bool?, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { +func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasUnreadTrending: Bool?, theme: PresentationTheme, hasGifs: Bool = true) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] if hasGifs { entries.append(.recentGifs(theme)) @@ -217,6 +217,21 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere return entries } +private let reactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"] + +func chatMediaInputPanelGifModeEntries(theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { + var entries: [ChatMediaInputPanelEntry] = [] + entries.append(.stickersMode(theme)) + entries.append(.savedGifs(theme)) + entries.append(.trendingGifs(theme)) + + for reaction in reactions { + entries.append(.gifEmotion(entries.count, theme, reaction)) + } + + return entries +} + func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { var entries: [ChatMediaInputGridEntry] = [] @@ -331,8 +346,16 @@ enum StickerPacksCollectionUpdate { case navigate(ItemCollectionViewEntryIndex?, ItemCollectionId?) } +enum ChatMediaInputGifMode: Equatable { + case recent + case trending + case emojiSearch(String) +} + final class ChatMediaInputNodeInteraction { let navigateToCollectionId: (ItemCollectionId) -> Void + let navigateBackToStickers: () -> Void + let setGifMode: (ChatMediaInputGifMode) -> Void let openSettings: () -> Void let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void let openPeerSpecificSettings: () -> Void @@ -342,11 +365,14 @@ final class ChatMediaInputNodeInteraction { var stickerSettings: ChatInterfaceStickerSettings? var highlightedStickerItemCollectionId: ItemCollectionId? var highlightedItemCollectionId: ItemCollectionId? + var highlightedGifMode: ChatMediaInputGifMode = .recent var previewedStickerPackItem: StickerPreviewPeekItem? var appearanceTransition: CGFloat = 1.0 - init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) { + init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, navigateBackToStickers: @escaping () -> Void, setGifMode: @escaping (ChatMediaInputGifMode) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) { self.navigateToCollectionId = navigateToCollectionId + self.navigateBackToStickers = navigateBackToStickers + self.setGifMode = setGifMode self.openSettings = openSettings self.toggleSearch = toggleSearch self.openPeerSpecificSettings = openPeerSpecificSettings @@ -413,6 +439,7 @@ final class ChatMediaInputNode: ChatInputNode { private let disposable = MetaDisposable() private let listView: ListView + private let gifListView: ListView private var searchContainerNode: PaneSearchContainerNode? private let searchContainerNodeLoadedDisposable = MetaDisposable() @@ -475,9 +502,12 @@ final class ChatMediaInputNode: ChatInputNode { self.listView = ListView() self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) + self.gifListView = ListView() + self.gifListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) + var paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)? var fixPaneScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void)? - var openGifContextMenuImpl: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? + var openGifContextMenuImpl: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? self.stickerPane = ChatMediaInputStickerPane(theme: theme, strings: strings, paneDidScroll: { pane, state, transition in paneDidScrollImpl?(pane, state, transition) @@ -488,8 +518,8 @@ final class ChatMediaInputNode: ChatInputNode { paneDidScrollImpl?(pane, state, transition) }, fixPaneScroll: { pane, state in fixPaneScrollImpl?(pane, state) - }, openGifContextMenu: { fileReference, sourceNode, sourceRect, gesture, isSaved in - openGifContextMenuImpl?(fileReference, sourceNode, sourceRect, gesture, isSaved) + }, openGifContextMenu: { file, sourceNode, sourceRect, gesture, isSaved in + openGifContextMenuImpl?(file, sourceNode, sourceRect, gesture, isSaved) }) var getItemIsPreviewedImpl: ((StickerPackItem) -> Bool)? @@ -546,6 +576,23 @@ final class ChatMediaInputNode: ChatInputNode { } } } + }, navigateBackToStickers: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) + }, setGifMode: { [weak self] mode in + guard let strongSelf = self else { + return + } + strongSelf.gifPane.setMode(mode: mode) + strongSelf.inputNodeInteraction.highlightedGifMode = strongSelf.gifPane.mode + + strongSelf.gifListView.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { + itemNode.updateIsHighlighted() + } + } }, openSettings: { [weak self] in if let strongSelf = self { let controller = installedStickerPacksController(context: context, mode: .modal) @@ -563,8 +610,8 @@ final class ChatMediaInputNode: ChatInputNode { self?.searchContainerNode?.deactivate() self?.inputNodeInteraction.toggleSearch(false, nil, "") }) - searchContainerNode?.openGifContextMenu = { fileReference, sourceNode, sourceRect, gesture, isSaved in - self?.openGifContextMenu(fileReference: fileReference, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) + searchContainerNode?.openGifContextMenu = { file, sourceNode, sourceRect, gesture, isSaved in + self?.openGifContextMenu(file: file, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) } strongSelf.searchContainerNode = searchContainerNode if !query.isEmpty { @@ -644,6 +691,8 @@ final class ChatMediaInputNode: ChatInputNode { self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor self.collectionListPanel.addSubnode(self.listView) + self.collectionListPanel.addSubnode(self.gifListView) + self.gifListView.isHidden = true self.collectionListContainer.addSubnode(self.collectionListPanel) self.collectionListContainer.addSubnode(self.collectionListSeparator) self.addSubnode(self.collectionListContainer) @@ -695,7 +744,7 @@ final class ChatMediaInputNode: ChatInputNode { } self.inputNodeInteraction.stickerSettings = self.controllerInteraction.stickerSettings - let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [])) + let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [], [])) let inputNodeInteraction = self.inputNodeInteraction! let peerSpecificPack: Signal<(PeerSpecificPackData?, CanInstallPeerSpecificPack), NoError> @@ -779,7 +828,7 @@ final class ChatMediaInputNode: ChatInputNode { let previousView = Atomic(value: nil) let transitionQueue = Queue() let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get()) - |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in + |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in let (view, viewUpdate) = viewAndUpdate let previous = previousView.swap(view) var update = viewUpdate @@ -815,6 +864,7 @@ final class ChatMediaInputNode: ChatInputNode { } let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, hasUnreadTrending: hasUnreadTrending, theme: theme) + let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme) var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, strings: strings, theme: theme) if view.higher == nil { @@ -833,21 +883,19 @@ final class ChatMediaInputNode: ChatInputNode { } } - let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries)) - return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) + let (previousPanelEntries, previousGifPaneEntries, previousGridEntries) = previousEntries.swap((panelEntries, gifPaneEntries, gridEntries)) + return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), preparedChatMediaInputPanelEntryTransition(context: context, from: previousGifPaneEntries, to: gifPaneEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) } self.disposable.set((transitions - |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in + |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, gifPaneTransition, panelFirstTime, gridTransition, gridFirstTime) in if let strongSelf = self { strongSelf.currentView = view strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime) + strongSelf.enqueueGifPanelTransition(gifPaneTransition, firstTime: false) if !strongSelf.initializedArrangement { strongSelf.initializedArrangement = true - var currentPane = strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] - if view.entries.isEmpty { - //currentPane = .trending - } + let currentPane = strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] if currentPane != strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] { strongSelf.setCurrentPane(currentPane, transition: .immediate) } @@ -869,7 +917,7 @@ final class ChatMediaInputNode: ChatInputNode { } } if let collectionId = topVisibleCollectionId { - if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId { + if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId && strongSelf.inputNodeInteraction.highlightedItemCollectionId?.namespace != ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue { strongSelf.setHighlightedItemCollectionId(collectionId) } } @@ -911,8 +959,8 @@ final class ChatMediaInputNode: ChatInputNode { self?.fixPaneScroll(pane: pane, state: state) } - openGifContextMenuImpl = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in - self?.openGifContextMenu(fileReference: fileReference, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) + openGifContextMenuImpl = { [weak self] file, sourceNode, sourceRect, gesture, isSaved in + self?.openGifContextMenu(file: file, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) } } @@ -921,9 +969,9 @@ final class ChatMediaInputNode: ChatInputNode { self.searchContainerNodeLoadedDisposable.dispose() } - private func openGifContextMenu(fileReference: FileMediaReference, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { + private func openGifContextMenu(file: MultiplexedVideoNodeFile, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { let canSaveGif: Bool - if fileReference.media.fileId.namespace == Namespaces.Media.CloudFile { + if file.file.media.fileId.namespace == Namespaces.Media.CloudFile { canSaveGif = true } else { canSaveGif = false @@ -933,14 +981,14 @@ final class ChatMediaInputNode: ChatInputNode { if !canSaveGif { return false } - return isGifSaved(transaction: transaction, mediaId: fileReference.media.fileId) + return isGifSaved(transaction: transaction, mediaId: file.file.media.fileId) } |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in guard let strongSelf = self else { return } - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [fileReference.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: []) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: []) let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in }, baseNavigationController: nil) @@ -949,7 +997,11 @@ final class ChatMediaInputNode: ChatInputNode { var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: strongSelf.strings.MediaPicker_Send, icon: { _ in nil }, action: { _, f in f(.default) - let _ = self?.controllerInteraction.sendGif(fileReference, sourceNode, sourceRect) + if isSaved { + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect) + } else if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect) + } }))) if isSaved || isGifSaved { items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { _ in nil }, action: { _, f in @@ -958,7 +1010,7 @@ final class ChatMediaInputNode: ChatInputNode { guard let strongSelf = self else { return } - let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: fileReference.media.fileId).start() + let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.file.media.fileId).start() }))) } else if canSaveGif && !isGifSaved { items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Preview_SaveGif, icon: { _ in nil }, action: { _, f in @@ -967,7 +1019,7 @@ final class ChatMediaInputNode: ChatInputNode { guard let strongSelf = self else { return } - let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: fileReference).start() + let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: file.file).start() }))) } @@ -1006,7 +1058,7 @@ final class ChatMediaInputNode: ChatInputNode { super.didLoad() self.view.disablesInteractiveTransitionGestureRecognizer = true - self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in + let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in if let strongSelf = self { let panes: [ASDisplayNode] if let searchContainerNode = strongSelf.searchContainerNode { @@ -1071,23 +1123,8 @@ final class ChatMediaInputNode: ChatInputNode { return nil } } - } else if let file = item as? FileMediaReference { + } else if let _ = item as? FileMediaReference { return nil - /*return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.context.account, contextResult: .internalReference(queryId: 0, id: "", type: "gif", title: nil, description: nil, image: nil, file: file.media, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [ - PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendGif(file, node, rect) - } else { - return false - } - }), - PeekControllerMenuItem(title: strongSelf.strings.Preview_SaveGif, color: .accent, action: { _, _ in - if let strongSelf = self { - let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: file).start() - } - return true - }) - ])))*/ } } } else { @@ -1103,21 +1140,6 @@ final class ChatMediaInputNode: ChatInputNode { if let pane = pane as? ChatMediaInputGifPane { if let (_, _, _) = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) { return nil - /*return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.context.account, contextResult: .internalReference(queryId: 0, id: "", type: "gif", title: nil, description: nil, image: nil, file: file.media, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [ - PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendGif(file, node, rect) - } else { - return false - } - }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Delete, color: .destructive, action: { _, _ in - if let strongSelf = self { - let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.media.fileId).start() - } - return true - }) - ])))*/ } } else if pane is ChatMediaInputStickerPane || pane is ChatMediaInputTrendingPane { var itemNodeAndItem: (ASDisplayNode, StickerPackItem)? @@ -1208,7 +1230,8 @@ final class ChatMediaInputNode: ChatInputNode { } strongSelf.updatePreviewingItem(item: item, animated: true) } - })) + }) + self.view.addGestureRecognizer(peekRecognizer) let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) self.panRecognizer = panRecognizer self.view.addGestureRecognizer(panRecognizer) @@ -1283,6 +1306,31 @@ final class ChatMediaInputNode: ChatInputNode { self.inputNodeInteraction.highlightedItemCollectionId = collectionId } } + + if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue && self.gifListView.isHidden { + self.listView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.bounds.width, y: 0.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak self] completed in + guard let strongSelf = self, completed else { + return + } + strongSelf.listView.isHidden = true + strongSelf.listView.layer.removeAllAnimations() + }) + self.gifListView.layer.removeAllAnimations() + self.gifListView.isHidden = false + self.gifListView.layer.animatePosition(from: CGPoint(x: -self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } else if !self.gifListView.isHidden { + self.gifListView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -self.bounds.width, y: 0.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak self] completed in + guard let strongSelf = self, completed else { + return + } + strongSelf.gifListView.isHidden = true + strongSelf.gifListView.layer.removeAllAnimations() + }) + self.listView.layer.removeAllAnimations() + self.listView.isHidden = false + self.listView.layer.animatePosition(from: CGPoint(x: self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + var ensuredNodeVisible = false var firstVisibleCollectionId: ItemCollectionId? self.listView.forEachItemNode { itemNode in @@ -1374,6 +1422,7 @@ final class ChatMediaInputNode: ChatInputNode { self.inputNodeInteraction.appearanceTransition = max(0.1, value) transition.updateAlpha(node: self.listView, alpha: value) + transition.updateAlpha(node: self.gifListView, alpha: value) self.listView.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode { itemNode.updateAppearanceTransition(transition: transition) @@ -1389,6 +1438,11 @@ final class ChatMediaInputNode: ChatInputNode { itemNode.updateAppearanceTransition(transition: transition) } } + self.gifListView.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { + itemNode.updateAppearanceTransition(transition: transition) + } + } } func simulateUpdateLayout(isVisible: Bool) { @@ -1486,11 +1540,16 @@ final class ChatMediaInputNode: ChatInputNode { self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width) transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) + self.gifListView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width) + transition.updatePosition(node: self.gifListView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 4.0 + leftInset, left: 0.0, bottom: 4.0 + rightInset, right: 0.0), duration: duration, curve: curve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.gifListView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + var visiblePanes: [(ChatMediaInputPaneType, CGFloat)] = [] var paneIndex = 0 @@ -1508,7 +1567,11 @@ final class ChatMediaInputNode: ChatInputNode { case .gifs: if self.gifPane.supernode == nil { if !displaySearch { - self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer) + if let searchContainerNode = self.searchContainerNode { + self.insertSubnode(self.gifPane, belowSubnode: searchContainerNode) + } else { + self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer) + } if self.searchContainerNode == nil { self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 0.0), size: CGSize(width: width, height: panelHeight)) } @@ -1520,22 +1583,17 @@ final class ChatMediaInputNode: ChatInputNode { } case .stickers: if self.stickerPane.supernode == nil { - self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer) + if let searchContainerNode = self.searchContainerNode { + self.insertSubnode(self.stickerPane, belowSubnode: searchContainerNode) + } else { + self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer) + } self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight)) } if self.stickerPane.frame != paneFrame { self.stickerPane.layer.removeAnimation(forKey: "position") transition.updateFrame(node: self.stickerPane, frame: paneFrame) } - /*case .trending: - if self.trendingPane.supernode == nil { - self.insertSubnode(self.trendingPane, belowSubnode: self.collectionListContainer) - self.trendingPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight)) - } - if self.trendingPane.frame != paneFrame { - self.trendingPane.layer.removeAnimation(forKey: "position") - transition.updateFrame(node: self.trendingPane, frame: paneFrame) - }*/ } } @@ -1694,6 +1752,14 @@ final class ChatMediaInputNode: ChatInputNode { }) } + private func enqueueGifPanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool) { + var options = ListViewDeleteAndInsertOptions() + options.insert(.Synchronous) + options.insert(.LowLatency) + self.gifListView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { _ in + }) + } + private func enqueueGridTransition(_ transition: ChatMediaInputGridTransition, firstTime: Bool) { var itemTransition: ContainedViewLayoutTransition = .immediate if transition.animated { @@ -1805,6 +1871,7 @@ final class ChatMediaInputNode: ChatInputNode { transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) + transition.updatePosition(node: self.gifListView, position: CGPoint(x: self.gifListView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) } private func fixPaneScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState) { @@ -1825,6 +1892,7 @@ final class ChatMediaInputNode: ChatInputNode { transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) + transition.updatePosition(node: self.gifListView, position: CGPoint(x: self.gifListView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -1833,7 +1901,8 @@ final class ChatMediaInputNode: ChatInputNode { return result } } - return super.hitTest(point, with: event) + let result = super.hitTest(point, with: event) + return result } static func setupPanelIconInsets(item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> UIEdgeInsets { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift b/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift index da90401935..25336491a5 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift @@ -25,72 +25,10 @@ enum ChatMediaInputPanelEntryStableId: Hashable { case peerSpecific case trending case settings - - static func ==(lhs: ChatMediaInputPanelEntryStableId, rhs: ChatMediaInputPanelEntryStableId) -> Bool { - switch lhs { - case .recentGifs: - if case .recentGifs = rhs { - return true - } else { - return false - } - case .savedStickers: - if case .savedStickers = rhs { - return true - } else { - return false - } - case .recentPacks: - if case .recentPacks = rhs { - return true - } else { - return false - } - case let .stickerPack(lhsId): - if case let .stickerPack(rhsId) = rhs, lhsId == rhsId { - return true - } else { - return false - } - case .peerSpecific: - if case .peerSpecific = rhs { - return true - } else { - return false - } - case .trending: - if case .trending = rhs { - return true - } else { - return false - } - case .settings: - if case .settings = rhs { - return true - } else { - return false - } - } - } - - var hashValue: Int { - switch self { - case .recentGifs: - return 0 - case .savedStickers: - return 1 - case .recentPacks: - return 2 - case .trending: - return 3 - case .settings: - return 4 - case .peerSpecific: - return 5 - case let .stickerPack(id): - return id.hashValue - } - } + case stickersMode + case savedGifs + case trendingGifs + case gifEmotion(String) } enum ChatMediaInputPanelEntry: Comparable, Identifiable { @@ -102,22 +40,35 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { case peerSpecific(theme: PresentationTheme, peer: Peer) case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme) + case stickersMode(PresentationTheme) + case savedGifs(PresentationTheme) + case trendingGifs(PresentationTheme) + case gifEmotion(Int, PresentationTheme, String) + var stableId: ChatMediaInputPanelEntryStableId { switch self { - case .recentGifs: - return .recentGifs - case .savedStickers: - return .savedStickers - case .recentPacks: - return .recentPacks - case .trending: - return .trending - case .settings: - return .settings - case .peerSpecific: - return .peerSpecific - case let .stickerPack(_, info, _, _): - return .stickerPack(info.id.id) + case .recentGifs: + return .recentGifs + case .savedStickers: + return .savedStickers + case .recentPacks: + return .recentPacks + case .trending: + return .trending + case .settings: + return .settings + case .peerSpecific: + return .peerSpecific + case let .stickerPack(_, info, _, _): + return .stickerPack(info.id.id) + case .stickersMode: + return .stickersMode + case .savedGifs: + return .savedGifs + case .trendingGifs: + return .trendingGifs + case let .gifEmotion(_, _, emoji): + return .gifEmotion(emoji) } } @@ -165,6 +116,30 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { } else { return false } + case let .stickersMode(lhsTheme): + if case let .stickersMode(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } + case let .savedGifs(lhsTheme): + if case let .savedGifs(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } + case let .trendingGifs(lhsTheme): + if case let .trendingGifs(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } + case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji): + if case let .gifEmotion(rhsIndex, rhsTheme, rhsEmoji) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsEmoji == rhsEmoji { + return true + } else { + return false + } } } @@ -222,6 +197,8 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { } else { return lhsIndex <= rhsIndex } + default: + return true } case let .trending(elevated, _): if elevated { @@ -238,8 +215,37 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return false } } - case .settings: + case .stickersMode: return false + case .savedGifs: + switch rhs { + case .savedGifs: + return false + default: + return true + } + case .trendingGifs: + switch rhs { + case .stickersMode, .savedGifs, .trendingGifs: + return false + default: + return true + } + case let .gifEmotion(lhsIndex, _, _): + switch rhs { + case .stickersMode, .savedGifs, .trendingGifs: + return false + case let .gifEmotion(rhsIndex, _, _): + return lhsIndex < rhsIndex + default: + return true + } + case .settings: + if case .settings = rhs { + return false + } else { + return true + } } } @@ -278,6 +284,22 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return ChatMediaInputStickerPackItem(account: context.account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, index: index, theme: theme, selected: { inputNodeInteraction.navigateToCollectionId(info.id) }) + case let .stickersMode(theme): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .stickersMode, theme: theme, selected: { + inputNodeInteraction.navigateBackToStickers() + }) + case let .savedGifs(theme): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedGifs, theme: theme, selected: { + inputNodeInteraction.setGifMode(.recent) + }) + case let .trendingGifs(theme): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .trendingGifs, theme: theme, selected: { + inputNodeInteraction.setGifMode(.trending) + }) + case let .gifEmotion(_, theme, emoji): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji), theme: theme, selected: { + inputNodeInteraction.setGifMode(.emojiSearch(emoji)) + }) } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 504eb8c5fe..f1c63f56e2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -1089,8 +1089,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if wideLayout { if let size = file.size { let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))" - if file.isAnimated && (!automaticDownload || !automaticPlayback) { - badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: "\(gifTitle) " + sizeString, size: nil, muted: false, active: false) + if file.isAnimated { + badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: "\(gifTitle)", size: nil, muted: false, active: false) } else if let duration = file.duration, !message.flags.contains(.Unsent) { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 77de0a793e..556c2b3e09 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -195,7 +195,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) }, openMessageContextActions: { _, _, _, _ in - }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in + }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in self?.openUrl(url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 401b709275..1027bccacf 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -71,7 +71,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil }, chatControllerNode: { @@ -188,6 +188,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { } } } + }, navigateBackToStickers: { + }, setGifMode: { _ in }, openSettings: { [weak self] in if let strongSelf = self { // let controller = installedStickerPacksController(context: context, mode: .modal) @@ -363,7 +365,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { } } - let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasGifs: false, hasUnreadTrending: hasUnreadTrending, theme: theme) + let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasUnreadTrending: hasUnreadTrending, theme: theme, hasGifs: false) var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, strings: strings, theme: theme) if view.higher == nil { diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift index a4b12e98f9..dd86061527 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift @@ -272,6 +272,10 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { let inputNodeInteraction = ChatMediaInputNodeInteraction( navigateToCollectionId: { _ in }, + navigateBackToStickers: { + }, + setGifMode: { _ in + }, openSettings: { }, toggleSearch: { _, _, _ in diff --git a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift index 32f2410679..6f397c1d61 100644 --- a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift @@ -11,9 +11,7 @@ import AccountContext import WebSearchUI import AppBundle -func paneGifSearchForQuery(account: Account, query: String, offset: String?, updateActivity: ((Bool) -> Void)?) -> Signal<([FileMediaReference], String?)?, NoError> { - let delayRequest = true - +func paneGifSearchForQuery(account: Account, query: String, offset: String?, incompleteResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> { let contextBot = account.postbox.transaction { transaction -> String in let configuration = currentSearchBotsConfiguration(transaction: transaction) return configuration.gifBotUsername ?? "gif" @@ -24,35 +22,23 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, upd |> mapToSignal { peerId -> Signal in if let peerId = peerId { return account.postbox.loadedPeerWithId(peerId) - |> map { peer -> Peer? in - return peer - } - |> take(1) + |> map { peer -> Peer? in + return peer + } + |> take(1) } else { return .single(nil) } } |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, offset: offset ?? "", limit: 50) + 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) } } - let botResult: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ previousResult in - var passthroughPreviousResult: ChatContextResultCollection? - if let previousResult = previousResult { - if case let .contextRequestResult(previousUser, previousResults) = previousResult { - if previousUser?.id == user.id { - passthroughPreviousResult = previousResults - } - } - } - return .contextRequestResult(nil, passthroughPreviousResult) - }) - let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> if delayRequest { maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue()) @@ -60,15 +46,16 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, upd maybeDelayedContextResults = results } - return botResult |> then(maybeDelayedContextResults) + return maybeDelayedContextResults } else { return .single({ _ in return nil }) } } return contextBot - |> mapToSignal { result -> Signal<([FileMediaReference], String?)?, NoError> in - if let r = result(nil), case let .contextRequestResult(_, collection) = r, let results = collection?.results { - var references: [FileMediaReference] = [] + |> mapToSignal { result -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> in + if let r = result(nil), case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection { + let results = collection.results + var references: [MultiplexedVideoNodeFile] = [] for result in results { switch result { case let .externalReference(externalReference): @@ -106,15 +93,17 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, upd } } 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)) + references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) } case let .internalReference(internalReference): if let file = internalReference.file { - references.append(FileMediaReference.standalone(media: file)) + references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) } } } - return .single((references, collection?.nextOffset)) + return .single((references, collection.nextOffset)) + } else if incompleteResults { + return .single(nil) } else { return .complete() } @@ -145,7 +134,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { private var validLayout: CGSize? - private let trendingPromise: Promise<[FileMediaReference]?> + private let trendingPromise: Promise<[MultiplexedVideoNodeFile]?> private let searchDisposable = MetaDisposable() private let _ready = Promise() @@ -156,11 +145,11 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { var deactivateSearchBar: (() -> Void)? var updateActivity: ((Bool) -> Void)? var requestUpdateQuery: ((String) -> Void)? - var openGifContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? + var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? private var hasInitialText = false - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[FileMediaReference]?>) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[MultiplexedVideoNodeFile]?>) { self.context = context self.controllerInteraction = controllerInteraction self.inputNodeInteraction = inputNodeInteraction @@ -198,13 +187,13 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { self.hasInitialText = true self.isLoadingNextResults = true - let signal: Signal<([FileMediaReference], String?)?, NoError> + let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError> if !text.isEmpty { 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 + |> map { items -> ([MultiplexedVideoNodeFile], String?)? in if let items = items { return (items, nil) } else { @@ -226,7 +215,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { } else { strongSelf.nextOffset = nil } - strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: result) + strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: result, isSearch: true), synchronous: true, resetScrollingToOffset: nil) strongSelf.updateActivity?(false) strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty })) @@ -241,7 +230,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { } self.isLoadingNextResults = true - let signal: Signal<([FileMediaReference], String?)?, NoError> + let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError> signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity) self.searchDisposable.set((signal @@ -251,12 +240,12 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { } var files = strongSelf.multiplexedNode?.files.trending ?? [] - var currentIds = Set(files.map { $0.media.fileId }) + var currentIds = Set(files.map { $0.file.media.fileId }) for item in result { - if currentIds.contains(item.media.fileId) { + if currentIds.contains(item.file.media.fileId) { continue } - currentIds.insert(item.media.fileId) + currentIds.insert(item.file.media.fileId) files.append(item) } @@ -266,7 +255,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { } else { strongSelf.nextOffset = nil } - strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: files) + strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: files, isSearch: true), synchronous: true, resetScrollingToOffset: nil) strongSelf.notFoundNode.isHidden = text.isEmpty || !files.isEmpty })) } @@ -326,8 +315,12 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { self.addSubnode(multiplexedNode) - multiplexedNode.fileSelected = { [weak self] fileReference, sourceNode, sourceRect in - let _ = self?.controllerInteraction.sendGif(fileReference, sourceNode, sourceRect) + multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in + if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect) + } else { + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect) + } } multiplexedNode.fileContextMenu = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in diff --git a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift index 4044f79d09..55453f9771 100644 --- a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift +++ b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift @@ -32,112 +32,39 @@ private final class VisibleVideoItem { case trending(MediaId) } let id: Id - let fileReference: FileMediaReference + let file: MultiplexedVideoNodeFile let frame: CGRect - init(fileReference: FileMediaReference, frame: CGRect, isTrending: Bool) { - self.fileReference = fileReference + init(file: MultiplexedVideoNodeFile, frame: CGRect, isTrending: Bool) { + self.file = file self.frame = frame if isTrending { - self.id = .trending(fileReference.media.fileId) + self.id = .trending(file.file.media.fileId) } else { - self.id = .saved(fileReference.media.fileId) + self.id = .saved(file.file.media.fileId) } } } +final class MultiplexedVideoNodeFile { + let file: FileMediaReference + let contextResult: (ChatContextResultCollection, ChatContextResult)? + + init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) { + self.file = file + self.contextResult = contextResult + } +} + final class MultiplexedVideoNodeFiles { - let saved: [FileMediaReference] - let trending: [FileMediaReference] + let saved: [MultiplexedVideoNodeFile] + let trending: [MultiplexedVideoNodeFile] + let isSearch: Bool - init(saved: [FileMediaReference], trending: [FileMediaReference]) { + init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool) { 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 - - self.titleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_TrendingSectionHeader, 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 = 8.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 + self.isSearch = isSearch } } @@ -168,13 +95,19 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } } - var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: []) { - didSet { - let startTime = CFAbsoluteTimeGetCurrent() - self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: true) - print("MultiplexedVideoNode files updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false) + + func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) { + self.files = files + + self.ignoreDidScroll = true + if let resetScrollingToOffset = resetScrollingToOffset { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y :resetScrollingToOffset) } + self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: synchronous) + self.ignoreDidScroll = false } + private var displayItems: [VisibleVideoItem] = [] private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:] private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:] @@ -185,7 +118,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:] private let savedTitleNode: ImmediateTextNode - private let trendingHeaderNode: TrendingHeaderNode + private let trendingTitleNode: ImmediateTextNode private var displayLink: CADisplayLink! private var timeOffset = 0.0 @@ -193,8 +126,8 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { private let timebase: CMTimebase - var fileSelected: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)? - var fileContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? + var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)? + var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? var enableVideoNodes = false init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { @@ -215,21 +148,18 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { self.savedTitleNode = ImmediateTextNode() self.savedTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_SavedSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) - self.trendingHeaderNode = TrendingHeaderNode() + self.trendingTitleNode = ImmediateTextNode() + self.trendingTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_TrendingSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) 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.scrollNode.addSubnode(self.trendingTitleNode) self.addSubnode(self.trackingNode) self.addSubnode(self.contextContainerNode) @@ -300,7 +230,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } self.contextContainerNode.customActivationProgress = { [weak self] progress, update in - guard let strongSelf = self, let gestureLocation = gestureLocation else { + guard let _ = self, let _ = gestureLocation else { return } /*let minScale: CGFloat = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width @@ -356,9 +286,13 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } } + private var ignoreDidScroll: Bool = false + func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.updateImmediatelyVisibleItems() - self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height) + if !self.ignoreDidScroll { + self.updateImmediatelyVisibleItems() + self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height) + } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { @@ -407,15 +341,12 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { thumbnailLayer.frame = item.frame } } else { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.fileReference, synchronousLoad: synchronous) + let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.file.file, synchronousLoad: synchronous) thumbnailLayer.frame = item.frame self.scrollNode.layer.addSublayer(thumbnailLayer) self.visibleThumbnailLayers[item.id] = thumbnailLayer } - let progressSize = CGSize(width: 24.0, height: 24.0) - let progressFrame = CGRect(origin: CGPoint(x: item.frame.midX - progressSize.width / 2.0, y: item.frame.midY - progressSize.height / 2.0), size: progressSize) - if item.frame.maxY < minVisibleY { continue } @@ -434,7 +365,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill layerHolder.layer.frame = item.frame self.scrollNode.layer.addSublayer(layerHolder.layer) - let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.fileReference, layerHolder: layerHolder) + let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.file.file, layerHolder: layerHolder) self.visibleLayers[item.id] = (manager, layerHolder) self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in if let strongSelf = self { @@ -490,11 +421,9 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { if !drawableSize.width.isZero { var displayItems: [VisibleVideoItem] = [] - let idealHeight = self.idealHeight - var verticalOffset: CGFloat = self.topInset - func commitFilesSpans(files: [FileMediaReference], isTrending: Bool) { + func commitFilesSpans(files: [MultiplexedVideoNodeFile], isTrending: Bool) { var rowsCount = 0 var firstRowMax = 0; @@ -512,7 +441,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { for a in 0 ..< itemsCount { var size: CGSize - if let dimensions = files[a].media.dimensions { + if let dimensions = files[a].file.media.dimensions { size = dimensions.cgSize } else { size = CGSize(width: 100.0, height: 100.0) @@ -588,7 +517,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { if itemsToRow[index] != nil && currentRowHorizontalOffset + itemSize.width >= drawableSize.width - 10.0 { 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)) + displayItems.append(VisibleVideoItem(file: files[index], frame: CGRect(origin: CGPoint(x: currentRowHorizontalOffset, y: verticalOffset), size: itemSize), isTrending: isTrending)) currentRowHorizontalOffset += itemSize.width + 1.0 if itemsToRow[index] != nil { @@ -598,104 +527,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } } - func commitFiles(files: [FileMediaReference], isTrending: Bool) { - var weights: [Int] = [] - var totalItemSize: CGFloat = 0.0 - for item in files { - let aspectRatio: CGFloat - if let dimensions = item.media.dimensions { - aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height - } else { - aspectRatio = 1.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: verticalOffset) - var previousItemSize: CGFloat = 0.0 - 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 { - 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 - } - - var rowSize = drawableSize.width - (CGFloat(row.count - 1) * minimumInteritemSpacing) - - 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 = 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 - } - } - + var hasContent = false if !self.files.saved.isEmpty { self.savedTitleNode.isHidden = false let leftInset: CGFloat = 10.0 @@ -703,19 +535,26 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { 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) + hasContent = true } 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 + if self.files.isSearch { + self.trendingTitleNode.isHidden = true + } else { + self.trendingTitleNode.isHidden = false + if hasContent { + verticalOffset += 15.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) - //commitFiles(files: self.files.trending, isTrending: true) } else { - self.trendingHeaderNode.isHidden = true + self.trendingTitleNode.isHidden = true } let contentSize = CGSize(width: drawableSize.width, height: verticalOffset + self.bottomInset) @@ -748,19 +587,19 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { func frameForItem(_ id: MediaId) -> CGRect? { for item in self.displayItems { - if item.fileReference.media.fileId == id { + if item.file.file.media.fileId == id { return item.frame } } return nil } - func fileAt(point: CGPoint) -> (FileMediaReference, CGRect, Bool)? { + func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? { let offsetPoint = point.offsetBy(dx: 0.0, dy: self.scrollNode.bounds.minY) return self.offsetFileAt(point: offsetPoint) } - private func offsetFileAt(point: CGPoint) -> (FileMediaReference, CGRect, Bool)? { + private func offsetFileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? { for item in self.displayItems { if item.frame.contains(point) { let isSaved: Bool @@ -770,7 +609,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { case .trending: isSaved = false } - return (item.fileReference, item.frame, isSaved) + return (item.file, item.frame, isSaved) } } return nil diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift index a3446ce1f2..25954ab938 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift @@ -74,6 +74,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu return false }, sendGif: { _, _, _ in return false + }, sendBotContextResultAsGif: { _, _, _, _ in + return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in diff --git a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift index dbdbab9121..49b617b275 100644 --- a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift @@ -39,13 +39,13 @@ final class PaneSearchContainerNode: ASDisplayNode { private var validLayout: CGSize? - var openGifContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? + var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? var ready: Signal { return self.contentNode.ready } - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<[FileMediaReference]?>, cancel: @escaping () -> Void) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<[MultiplexedVideoNodeFile]?>, cancel: @escaping () -> Void) { self.context = context self.mode = mode self.controllerInteraction = controllerInteraction diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index e6ac8a5a1a..62ee75c7b3 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1479,6 +1479,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD return false }, sendGif: { _, _, _ in return false + }, sendBotContextResultAsGif: { _, _, _, _ in + return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in diff --git a/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift b/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift index 1c6a770ead..8a4688ae51 100644 --- a/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift @@ -325,6 +325,8 @@ public class PeerMediaCollectionController: TelegramBaseController { return false }, sendGif: { _, _, _ in return false + }, sendBotContextResultAsGif: { _, _, _, _ in + return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index ffb38834ad..3c719e1c29 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1106,7 +1106,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { tapMessage?(message) }, clickThroughMessage: { clickThroughMessage?() - }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in + return false + }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil }, chatControllerNode: { diff --git a/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift b/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift index c5b6c96337..eefc709bef 100644 --- a/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift +++ b/submodules/TelegramUI/Sources/SoftwareVideoLayerFrameManager.swift @@ -109,6 +109,7 @@ final class SoftwareVideoLayerFrameManager { while index < self.frames.count { if baseTimestamp + self.frames[index].position.seconds + self.frames[index].duration.seconds <= timestamp { latestFrameIndex = index + //print("latestFrameIndex = \(index)") } index += 1 } @@ -139,7 +140,7 @@ final class SoftwareVideoLayerFrameManager { private var polling = false private func poll() { - if self.frames.count < 3 && !self.polling { + if self.frames.count < 2 && !self.polling { self.polling = true let minPts = self.minPts let maxPts = self.maxPts @@ -179,7 +180,11 @@ final class SoftwareVideoLayerFrameManager { } if let frame = frameAndLoop?.0 { if strongSelf.minPts == nil || CMTimeCompare(strongSelf.minPts!, frame.position) < 0 { - strongSelf.minPts = frame.position + var position = CMTimeAdd(frame.position, frame.duration) + for _ in 0 ..< 1 { + position = CMTimeAdd(position, frame.duration) + } + strongSelf.minPts = position } strongSelf.frames.append(frame) strongSelf.frames.sort(by: { lhs, rhs in @@ -190,7 +195,7 @@ final class SoftwareVideoLayerFrameManager { } }) //print("add frame at \(CMTimeGetSeconds(frame.position))") - let positions = strongSelf.frames.map { CMTimeGetSeconds($0.position) } + //let positions = strongSelf.frames.map { CMTimeGetSeconds($0.position) } //print("frames: \(positions)") } else { //print("not adding frames") diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift index 9bb973d8d7..d5f037e7c6 100644 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchController.swift @@ -10,8 +10,8 @@ import LegacyComponents import TelegramUIPreferences import AccountContext -public func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, limit: Int = 60) -> Signal { - return requestChatContextResults(account: account, botId: botId, peerId: peerId, query: query, offset: offset) +public func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, limit: Int = 60) -> Signal { + return requestChatContextResults(account: account, botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults) |> `catch` { error -> Signal in return .single(nil) }