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 382c35e033..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@3x.png deleted file mode 100644 index 38c0a2480d..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@3x.png and /dev/null differ 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 0000000000..50f89933d7 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/ic_input_gifs.pdf differ 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 371aa0aee9..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@3x.png deleted file mode 100644 index 045a394615..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@3x.png and /dev/null differ 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 cbe24a9aa0..7afd3b024f 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_addstickers.pdf and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/ic_input_recent.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/Contents.json new file mode 100644 index 0000000000..06c3a823d3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_input_stickers.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/ic_input_stickers.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/ic_input_stickers.pdf new file mode 100644 index 0000000000..67ae05a315 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/ic_input_stickers.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/Contents.json new file mode 100644 index 0000000000..7e3c34b404 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_input_trending.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/ic_input_trending.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/ic_input_trending.pdf new file mode 100644 index 0000000000..79a53a462c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/ic_input_trending.pdf differ 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 0000000000..4e0a98ec48 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_input_addstickers.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index abbff3f33c..637715ae1a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -766,6 +766,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } + return true + }, sendBotContextResultAsGif: { [weak self] collection, result, sourceNode, sourceRect in + guard let strongSelf = self else { + return false + } + if let _ = strongSelf.presentationInterfaceState.slowmodeState, !strongSelf.presentationInterfaceState.isScheduledMessages { + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect) + return false + } + + strongSelf.enqueueChatContextResult(collection, result, hideVia: true, closeMediaInput: true) + return true }, requestMessageActionCallback: { [weak self] messageId, data, isGame in if let strongSelf = self { @@ -6946,16 +6958,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false) { + private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false) { guard case let .peer(peerId) = self.chatLocation else { return } + if let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result, hideVia: hideVia), canSendMessagesToChat(self.presentationInterfaceState) { let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + var state = state + state = state.updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + interfaceState = interfaceState.withUpdatedReplyMessageId(nil) + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) + interfaceState = interfaceState.withUpdatedComposeDisableUrlPreview(nil) + return interfaceState + } + state = state.updatedInputMode { current in + if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil) + } + return current + } + return state }) } }) diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 46e1b9c4b9..05d5e4ba79 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -70,6 +70,7 @@ public final class ChatControllerInteraction { let sendMessage: (String) -> 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) }