GIF-related fixes part 2

This commit is contained in:
Ali 2020-05-26 03:53:24 +04:00
parent 03e773580d
commit c4dd56b596
47 changed files with 893 additions and 568 deletions

View File

@ -4,6 +4,13 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, FFMpegAVCodecContextReceiveResult)
{
FFMpegAVCodecContextReceiveResultError,
FFMpegAVCodecContextReceiveResultNotEnoughData,
FFMpegAVCodecContextReceiveResultSuccess,
};
@class FFMpegAVCodec; @class FFMpegAVCodec;
@class FFMpegAVFrame; @class FFMpegAVFrame;
@ -17,7 +24,8 @@ NS_ASSUME_NONNULL_BEGIN
- (FFMpegAVSampleFormat)sampleFormat; - (FFMpegAVSampleFormat)sampleFormat;
- (bool)open; - (bool)open;
- (bool)receiveIntoFrame:(FFMpegAVFrame *)frame; - (bool)sendEnd;
- (FFMpegAVCodecContextReceiveResult)receiveIntoFrame:(FFMpegAVFrame *)frame;
- (void)flushBuffers; - (void)flushBuffers;
@end @end

View File

@ -14,6 +14,7 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFrameColorRange) {
@property (nonatomic, readonly) uint8_t **data; @property (nonatomic, readonly) uint8_t **data;
@property (nonatomic, readonly) int *lineSize; @property (nonatomic, readonly) int *lineSize;
@property (nonatomic, readonly) int64_t pts; @property (nonatomic, readonly) int64_t pts;
@property (nonatomic, readonly) int64_t duration;
@property (nonatomic, readonly) FFMpegAVFrameColorRange colorRange; @property (nonatomic, readonly) FFMpegAVFrameColorRange colorRange;
- (instancetype)init; - (instancetype)init;

View File

@ -50,11 +50,22 @@
return result >= 0; return result >= 0;
} }
- (bool)receiveIntoFrame:(FFMpegAVFrame *)frame { - (bool)sendEnd {
int status = avcodec_receive_frame(_impl, (AVFrame *)[frame impl]); int status = avcodec_send_packet(_impl, nil);
return status == 0; 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 { - (void)flushBuffers {
avcodec_flush_buffers(_impl); avcodec_flush_buffers(_impl);
} }

View File

@ -44,6 +44,10 @@
return _impl->pts; return _impl->pts;
} }
- (int64_t)duration {
return _impl->pkt_duration;
}
- (FFMpegAVFrameColorRange)colorRange { - (FFMpegAVFrameColorRange)colorRange {
switch (_impl->color_range) { switch (_impl->color_range) {
case AVCOL_RANGE_MPEG: case AVCOL_RANGE_MPEG:

View File

@ -21,10 +21,15 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? { func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? {
let status = frame.packet.send(toDecoder: self.codecContext) let status = frame.packet.send(toDecoder: self.codecContext)
if status == 0 { if status == 0 {
while self.codecContext.receive(into: self.audioFrame) { 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) { if let convertedFrame = convertAudioFrame(self.audioFrame, pts: frame.pts, duration: frame.duration) {
self.delayedFrames.append(convertedFrame) self.delayedFrames.append(convertedFrame)
} }
} else {
break
}
} }
if self.delayedFrames.count >= 1 { if self.delayedFrames.count >= 1 {

View File

@ -17,11 +17,20 @@ private let deviceColorSpace: CGColorSpace = {
}() }()
public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
public enum ReceiveResult {
case error
case moreDataNeeded
case result(MediaTrackFrame)
}
private let codecContext: FFMpegAVCodecContext private let codecContext: FFMpegAVCodecContext
private let videoFrame: FFMpegAVFrame private let videoFrame: FFMpegAVFrame
private var resetDecoderOnNextFrame = true private var resetDecoderOnNextFrame = true
private var defaultDuration: CMTime?
private var defaultTimescale: CMTimeScale?
private var pixelBufferPool: CVPixelBufferPool? private var pixelBufferPool: CVPixelBufferPool?
private var delayedFrames: [MediaTrackFrame] = [] private var delayedFrames: [MediaTrackFrame] = []
@ -60,10 +69,51 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
return self.decode(frame: frame, ptsOffset: nil) 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? { public func decode(frame: MediaTrackDecodableFrame, ptsOffset: CMTime?) -> MediaTrackFrame? {
let status = frame.packet.send(toDecoder: self.codecContext) let status = frame.packet.send(toDecoder: self.codecContext)
if status == 0 { 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) var pts = CMTimeMake(value: self.videoFrame.pts, timescale: frame.pts.timescale)
if let ptsOffset = ptsOffset { if let ptsOffset = ptsOffset {
pts = CMTimeAdd(pts, ptsOffset) pts = CMTimeAdd(pts, ptsOffset)
@ -75,10 +125,35 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
return nil 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? { public func render(frame: MediaTrackDecodableFrame) -> UIImage? {
let status = frame.packet.send(toDecoder: self.codecContext) let status = frame.packet.send(toDecoder: self.codecContext)
if status == 0 { if status == 0 {
if self.codecContext.receive(into: self.videoFrame) { if case .success = self.codecContext.receive(into: self.videoFrame) {
return convertVideoFrameToImage(self.videoFrame) return convertVideoFrameToImage(self.videoFrame)
} }
} }

View File

@ -54,6 +54,9 @@ public final class SoftwareVideoSource {
fileprivate let fd: Int32? fileprivate let fd: Int32?
fileprivate let size: Int32 fileprivate let size: Int32
private var enqueuedFrames: [(MediaTrackFrame, CGFloat, CGFloat, Bool)] = []
private var hasReadToEnd: Bool = false
public init(path: String) { public init(path: String) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
@ -178,7 +181,7 @@ public final class SoftwareVideoSource {
} else { } else {
if let avFormatContext = self.avFormatContext, let videoStream = self.videoStream { if let avFormatContext = self.avFormatContext, let videoStream = self.videoStream {
endOfStream = true endOfStream = true
avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true) break
} else { } else {
endOfStream = true endOfStream = true
break break
@ -187,30 +190,44 @@ public final class SoftwareVideoSource {
} }
} }
if endOfStream {
if let videoStream = self.videoStream {
videoStream.decoder.reset()
}
}
return (frames.first, endOfStream) return (frames.first, endOfStream)
} }
public func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) { public func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) {
if let videoStream = self.videoStream { 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() let (decodableFrame, loop) = self.readDecodableFrame()
var result: (MediaTrackFrame?, CGFloat, CGFloat, Bool)
if let decodableFrame = decodableFrame { if let decodableFrame = decodableFrame {
var ptsOffset: CMTime? var ptsOffset: CMTime?
if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 { if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 {
ptsOffset = maxPts ptsOffset = maxPts
} }
return (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) result = (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
} else { } else {
return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
} }
} else { if loop {
return (nil, 0.0, 1.0, false) 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) { public func readImage() -> (UIImage?, CGFloat, CGFloat, Bool) {

View File

@ -130,11 +130,26 @@ final class MessageHistoryHoleIndexTable: Table {
if !self.metadataTable.isInitialized(peerId) { if !self.metadataTable.isInitialized(peerId) {
self.metadataTable.setInitialized(peerId) self.metadataTable.setInitialized(peerId)
if let tagsByNamespace = self.seedConfiguration.messageHoles[peerId.namespace] { 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]] = [:] var operations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:]
self.add(peerId: peerId, namespace: namespace, space: .everywhere, range: 1 ... (Int32.max - 1), operations: &operations) 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)
}
}
}
}
} }
} }

View File

@ -12,6 +12,7 @@ private enum MetadataPrefix: Int8 {
case PeerHistoryInitialized = 9 case PeerHistoryInitialized = 9
case ShouldReindexUnreadCountsState = 10 case ShouldReindexUnreadCountsState = 10
case TotalUnreadCountStates = 11 case TotalUnreadCountStates = 11
case PeerHistoryTagInitialized = 12
} }
public struct ChatListTotalUnreadCounters: PostboxCoding, Equatable { public struct ChatListTotalUnreadCounters: PostboxCoding, Equatable {
@ -51,6 +52,7 @@ final class MessageHistoryMetadataTable: Table {
private var initializedChatList = Set<InitializedChatListKey>() private var initializedChatList = Set<InitializedChatListKey>()
private var initializedHistoryPeerIds = Set<PeerId>() private var initializedHistoryPeerIds = Set<PeerId>()
private var initializedHistoryPeerIdTags: [PeerId: Set<MessageTags>] = [:]
private var initializedGroupFeedIndexIds = Set<PeerGroupId>() private var initializedGroupFeedIndexIds = Set<PeerGroupId>()
private var peerNextMessageIdByNamespace: [PeerId: [MessageId.Namespace: MessageId.Id]] = [:] private var peerNextMessageIdByNamespace: [PeerId: [MessageId.Namespace: MessageId.Id]] = [:]
@ -74,6 +76,14 @@ final class MessageHistoryMetadataTable: Table {
return self.sharedPeerHistoryInitializedKey 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 { private func groupFeedIndexInitializedKey(_ id: PeerGroupId) -> ValueBoxKey {
self.sharedGroupFeedIndexInitializedKey.setInt32(0, value: id.rawValue) self.sharedGroupFeedIndexInitializedKey.setInt32(0, value: id.rawValue)
self.sharedGroupFeedIndexInitializedKey.setInt8(4, value: MetadataPrefix.GroupFeedIndexInitialized.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) { func setGroupFeedIndexInitialized(_ groupId: PeerGroupId) {
self.initializedGroupFeedIndexIds.insert(groupId) self.initializedGroupFeedIndexIds.insert(groupId)
self.sharedBuffer.reset() self.sharedBuffer.reset()

View File

@ -59,6 +59,7 @@ public final class SeedConfiguration {
public let globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace> public let globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>
public let initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?) public let initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?)
public let messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]] public let messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]]
public let upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]]
public let messageTagsWithSummary: MessageTags public let messageTagsWithSummary: MessageTags
public let existingGlobalMessageTags: GlobalMessageTags public let existingGlobalMessageTags: GlobalMessageTags
public let peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace] public let peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace]
@ -70,10 +71,11 @@ public final class SeedConfiguration {
public let globalNotificationSettingsPreferencesKey: ValueBoxKey public let globalNotificationSettingsPreferencesKey: ValueBoxKey
public let defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings public let defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings
public init(globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer, Bool) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState], chatMessagesNamespaces: Set<MessageId.Namespace>, globalNotificationSettingsPreferencesKey: ValueBoxKey, defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings) { public init(globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer, Bool) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState], chatMessagesNamespaces: Set<MessageId.Namespace>, globalNotificationSettingsPreferencesKey: ValueBoxKey, defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings) {
self.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces self.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces
self.initializeChatListWithHole = initializeChatListWithHole self.initializeChatListWithHole = initializeChatListWithHole
self.messageHoles = messageHoles self.messageHoles = messageHoles
self.upgradedMessageHoles = upgradedMessageHoles
self.messageTagsWithSummary = messageTagsWithSummary self.messageTagsWithSummary = messageTagsWithSummary
self.existingGlobalMessageTags = existingGlobalMessageTags self.existingGlobalMessageTags = existingGlobalMessageTags
self.peerNamespacesRequiringMessageTextIndex = peerNamespacesRequiringMessageTextIndex self.peerNamespacesRequiringMessageTextIndex = peerNamespacesRequiringMessageTextIndex

View File

@ -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<MessageTags>]] = [:]
for peerNamespace in peerIdNamespacesWithInitialCloudMessageHoles {
upgradedMessageHoles[peerNamespace] = [
Namespaces.Message.Cloud: Set(MessageTags.gif)
]
}
var globalMessageIdsPeerIdNamespaces = Set<GlobalMessageIdsNamespace>() var globalMessageIdsPeerIdNamespaces = Set<GlobalMessageIdsNamespace>()
for peerIdNamespace in [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup] { for peerIdNamespace in [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup] {
globalMessageIdsPeerIdNamespaces.insert(GlobalMessageIdsNamespace(peerIdNamespace: peerIdNamespace, messageIdNamespace: Namespaces.Message.Cloud)) 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 let peer = peer as? TelegramUser {
if peer.botInfo != nil { if peer.botInfo != nil {
return .bot return .bot

View File

@ -41,7 +41,7 @@ private struct RequestData: Codable {
private let requestVersion = "3" 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<ChatContextResultCollection?, RequestChatContextResultsError> { public func requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false) -> Signal<ChatContextResultCollection?, RequestChatContextResultsError> {
return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in
if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) { if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) {
return (bot, peer) return (bot, peer)
@ -92,7 +92,9 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P
flags |= (1 << 0) flags |= (1 << 0)
geoPoint = Api.InputGeoPoint.inputGeoPoint(lat: latitude, long: longitude) 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<ChatContextResultCollection?, RequestChatContextResultsError> = account.network.request(Api.functions.messages.getInlineBotResults(flags: flags, bot: inputBot, peer: inputPeer, geoPoint: geoPoint, query: query, offset: offset))
|> map { result -> ChatContextResultCollection? in |> map { result -> ChatContextResultCollection? in
return ChatContextResultCollection(apiResults: result, botId: bot.id, peerId: peerId, query: query, geoPoint: location) 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) |> castError(RequestChatContextResultsError.self)
} }
if incompleteResults {
signal = .single(nil) |> then(signal)
}
return signal
} }
|> castError(RequestChatContextResultsError.self) |> castError(RequestChatContextResultsError.self)
|> switchToLatest |> switchToLatest

View File

@ -130,6 +130,8 @@ public enum PresentationResourceKey: Int32 {
case chatInputMediaPanelAddedPackButtonImage case chatInputMediaPanelAddedPackButtonImage
case chatInputMediaPanelGridSetupImage case chatInputMediaPanelGridSetupImage
case chatInputMediaPanelGridDismissImage case chatInputMediaPanelGridDismissImage
case chatInputMediaPanelTrendingGifsIcon
case chatInputMediaPanelStickersModeIcon
case chatInputButtonPanelButtonImage case chatInputButtonPanelButtonImage
case chatInputButtonPanelButtonHighlightedImage case chatInputButtonPanelButtonHighlightedImage

View File

@ -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? { public static func chatInputMediaPanelRecentStickersIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatInputMediaPanelRecentStickersIconImage.rawValue, { theme in return theme.image(PresentationResourceKey.chatInputMediaPanelRecentStickersIconImage.rawValue, { theme in
return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in

View File

@ -1,22 +1,12 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "ic_input_gifs.pdf",
"scale" : "1x" "idiom" : "universal"
},
{
"idiom" : "universal",
"filename" : "StickerKeyboardGifIcon@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "StickerKeyboardGifIcon@3x.png",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,22 +1,12 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "ic_input_recent.pdf",
"scale" : "1x" "idiom" : "universal"
},
{
"idiom" : "universal",
"filename" : "StickerKeyboardRecentTab@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "StickerKeyboardRecentTab@3x.png",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_input_stickers.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_input_trending.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,12 +1,12 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "ic_input_addstickers.pdf",
"filename" : "ic_addstickers.pdf" "idiom" : "universal"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

View File

@ -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)]) 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 return true
}, requestMessageActionCallback: { [weak self] messageId, data, isGame in }, requestMessageActionCallback: { [weak self] messageId, data, isGame in
if let strongSelf = self { 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 { guard case let .peer(peerId) = self.chatLocation else {
return return
} }
if let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result, hideVia: hideVia), canSendMessagesToChat(self.presentationInterfaceState) { if let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result, hideVia: hideVia), canSendMessagesToChat(self.presentationInterfaceState) {
let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } 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
}) })
} }
}) })

View File

@ -70,6 +70,7 @@ public final class ChatControllerInteraction {
let sendMessage: (String) -> Void let sendMessage: (String) -> Void
let sendSticker: (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool let sendSticker: (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool
let sendGif: (FileMediaReference, ASDisplayNode, CGRect) -> Bool let sendGif: (FileMediaReference, ASDisplayNode, CGRect) -> Bool
let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool
let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void
let requestMessageActionUrlAuth: (String, MessageId, Int32) -> Void let requestMessageActionUrlAuth: (String, MessageId, Int32) -> Void
let activateSwitchInline: (PeerId?, String) -> Void let activateSwitchInline: (PeerId?, String) -> Void
@ -135,7 +136,7 @@ public final class ChatControllerInteraction {
var searchTextHighightState: (String, [MessageIndex])? var searchTextHighightState: (String, [MessageIndex])?
var seenOneTimeAnimatedMedia = Set<MessageId>() var seenOneTimeAnimatedMedia = Set<MessageId>()
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.openMessage = openMessage
self.openPeer = openPeer self.openPeer = openPeer
self.openPeerMention = openPeerMention self.openPeerMention = openPeerMention
@ -149,6 +150,7 @@ public final class ChatControllerInteraction {
self.sendMessage = sendMessage self.sendMessage = sendMessage
self.sendSticker = sendSticker self.sendSticker = sendSticker
self.sendGif = sendGif self.sendGif = sendGif
self.sendBotContextResultAsGif = sendBotContextResultAsGif
self.requestMessageActionCallback = requestMessageActionCallback self.requestMessageActionCallback = requestMessageActionCallback
self.requestMessageActionUrlAuth = requestMessageActionUrlAuth self.requestMessageActionUrlAuth = requestMessageActionUrlAuth
self.activateSwitchInline = activateSwitchInline self.activateSwitchInline = activateSwitchInline
@ -209,7 +211,7 @@ public final class ChatControllerInteraction {
static var `default`: ChatControllerInteraction { static var `default`: ChatControllerInteraction {
return ChatControllerInteraction(openMessage: { _, _ in 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: { }, presentController: { _, _ in }, navigationController: {
return nil return nil
}, chatControllerNode: { }, chatControllerNode: {

View File

@ -32,7 +32,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void
private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> 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 private let searchPlaceholderNode: PaneSearchBarPlaceholderNode
var visibleSearchPlaceholderNode: PaneSearchBarPlaceholderNode? { var visibleSearchPlaceholderNode: PaneSearchBarPlaceholderNode? {
@ -49,14 +49,16 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
private let emptyNode: ImmediateTextNode private let emptyNode: ImmediateTextNode
private let disposable = MetaDisposable() 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 validLayout: (CGSize, CGFloat, CGFloat, Bool, Bool, DeviceMetrics)?
private var didScrollPreviousOffset: CGFloat? private var didScrollPreviousOffset: CGFloat?
private var didScrollPreviousState: ChatMediaInputPaneScrollState? 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.account = account
self.theme = theme self.theme = theme
self.strings = strings 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.attributedText = NSAttributedString(string: strings.Gif_NoGifsPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
self.emptyNode.textAlignment = .center self.emptyNode.textAlignment = .center
self.emptyNode.maximumNumberOfLines = 3 self.emptyNode.maximumNumberOfLines = 3
self.emptyNode.isHidden = true
super.init() super.init()
@ -116,7 +119,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
self.updateMultiplexedNodeLayout(changedIsExpanded: changedIsExpanded, transition: transition) 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 { if let multiplexedNode = self.multiplexedNode {
return multiplexedNode.fileAt(point: point.offsetBy(dx: -multiplexedNode.frame.minX, dy: -multiplexedNode.frame.minY)) return multiplexedNode.fileAt(point: point.offsetBy(dx: -multiplexedNode.frame.minX, dy: -multiplexedNode.frame.minY))
} else { } 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 { override var isEmpty: Bool {
if let files = self.multiplexedNode?.files { if let files = self.multiplexedNode?.files {
return files.trending.isEmpty && files.saved.isEmpty return files.trending.isEmpty && files.saved.isEmpty
@ -139,13 +150,23 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
} }
private func updateMultiplexedNodeLayout(changedIsExpanded: Bool, transition: ContainedViewLayoutTransition) { 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 return
} }
if let multiplexedNode = self.multiplexedNode { if let multiplexedNode = self.multiplexedNode {
let previousBounds = multiplexedNode.scrollNode.layer.bounds let _ = multiplexedNode.scrollNode.layer.bounds
multiplexedNode.topInset = topInset + 60.0
let displaySearch: Bool
switch self.mode {
case .recent:
displaySearch = true
default:
displaySearch = false
}
multiplexedNode.topInset = topInset + (displaySearch ? 60.0 : 0.0)
multiplexedNode.bottomInset = bottomInset multiplexedNode.bottomInset = bottomInset
if case .tablet = deviceMetrics.type, size.width > 480.0 { 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)) 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 { if changedIsExpanded {
let isEmpty = multiplexedNode.files.trending.isEmpty && multiplexedNode.files.saved.isEmpty let isEmpty = multiplexedNode.files.trending.isEmpty && multiplexedNode.files.saved.isEmpty
//targetBounds.origin.y = isExpanded || isEmpty ? 0.0 : 60.0 //targetBounds.origin.y = isExpanded || isEmpty ? 0.0 : 60.0
} }*/
//transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: targetBounds) //transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: targetBounds)
transition.updateFrame(node: multiplexedNode, frame: nodeFrame) transition.updateFrame(node: multiplexedNode, frame: nodeFrame)
@ -172,8 +193,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
func initializeIfNeeded() { func initializeIfNeeded() {
if self.multiplexedNode == nil { if self.multiplexedNode == nil {
self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, updateActivity: nil) self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil)
|> map { items -> [FileMediaReference]? in |> map { items -> [MultiplexedVideoNodeFile]? in
if let (items, _) = items { if let (items, _) = items {
return items return items
} else { } else {
@ -197,47 +218,12 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
self.addSubnode(multiplexedNode) self.addSubnode(multiplexedNode)
multiplexedNode.scrollNode.addSubnode(self.searchPlaceholderNode) multiplexedNode.scrollNode.addSubnode(self.searchPlaceholderNode)
let gifs = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)])) multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in
|> map { trending, view -> MultiplexedVideoNodeFiles in if let (collection, result) = file.contextResult {
var recentGifs: OrderedItemListView? let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect)
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)
}
} else { } 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 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.updateMultiplexedNodeLayout(changedIsExpanded: false, transition: .immediate)
self.resetMode(synchronous: false)
} }
} }
private func resetMode(synchronous: Bool) {
let filesSignal: Signal<MultiplexedVideoNodeFiles, NoError>
switch self.mode {
case .recent:
filesSignal = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]))
|> map { trending, view -> MultiplexedVideoNodeFiles in
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*/
}
}))
}
} }

View File

@ -8,9 +8,13 @@ import SwiftSignalKit
import Postbox import Postbox
import TelegramPresentationData import TelegramPresentationData
enum ChatMediaInputMetaSectionItemType { enum ChatMediaInputMetaSectionItemType: Equatable {
case savedStickers case savedStickers
case recentStickers case recentStickers
case stickersMode
case savedGifs
case trendingGifs
case gifEmoji(String)
} }
final class ChatMediaInputMetaSectionItem: ListViewItem { final class ChatMediaInputMetaSectionItem: ListViewItem {
@ -20,7 +24,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
let selectedItem: () -> Void let selectedItem: () -> Void
var selectable: Bool { var selectable: Bool {
return true return false
} }
init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, selected: @escaping () -> Void) { init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, selected: @escaping () -> Void) {
@ -34,7 +38,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
async { async {
let node = ChatMediaInputMetaSectionItemNode() let node = ChatMediaInputMetaSectionItemNode()
node.contentSize = CGSize(width: 41.0, height: 41.0) 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.inputNodeInteraction = self.inputNodeInteraction
node.setItem(item: self) node.setItem(item: self)
node.updateTheme(theme: self.theme) node.updateTheme(theme: self.theme)
@ -71,7 +75,10 @@ private let verticalOffset: CGFloat = 3.0 + UIScreenPixel
final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
private let imageNode: ASImageNode private let imageNode: ASImageNode
private let textNodeContainer: ASDisplayNode
private let textNode: ImmediateTextNode
private let highlightNode: ASImageNode private let highlightNode: ASImageNode
private let buttonNode: HighlightTrackingButtonNode
var item: ChatMediaInputMetaSectionItem? var item: ChatMediaInputMetaSectionItem?
var currentCollectionId: ItemCollectionId? var currentCollectionId: ItemCollectionId?
@ -87,17 +94,46 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
self.imageNode = ASImageNode() self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true 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.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.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) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.highlightNode) self.addSubnode(self.highlightNode)
self.addSubnode(self.imageNode) self.addSubnode(self.imageNode)
self.addSubnode(self.textNodeContainer)
self.addSubnode(self.buttonNode)
let imageSize = CGSize(width: 26.0, height: 26.0) 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.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) { func setItem(item: ChatMediaInputMetaSectionItem) {
@ -107,6 +143,8 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0) self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0)
case .recentStickers: case .recentStickers:
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0)
default:
break
} }
} }
@ -121,14 +159,47 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme) self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme)
case .recentStickers: case .recentStickers:
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme) 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() { 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 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
} }
} }

View File

@ -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) 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] = [] var entries: [ChatMediaInputPanelEntry] = []
if hasGifs { if hasGifs {
entries.append(.recentGifs(theme)) entries.append(.recentGifs(theme))
@ -217,6 +217,21 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere
return entries 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] { func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] {
var entries: [ChatMediaInputGridEntry] = [] var entries: [ChatMediaInputGridEntry] = []
@ -331,8 +346,16 @@ enum StickerPacksCollectionUpdate {
case navigate(ItemCollectionViewEntryIndex?, ItemCollectionId?) case navigate(ItemCollectionViewEntryIndex?, ItemCollectionId?)
} }
enum ChatMediaInputGifMode: Equatable {
case recent
case trending
case emojiSearch(String)
}
final class ChatMediaInputNodeInteraction { final class ChatMediaInputNodeInteraction {
let navigateToCollectionId: (ItemCollectionId) -> Void let navigateToCollectionId: (ItemCollectionId) -> Void
let navigateBackToStickers: () -> Void
let setGifMode: (ChatMediaInputGifMode) -> Void
let openSettings: () -> Void let openSettings: () -> Void
let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void
let openPeerSpecificSettings: () -> Void let openPeerSpecificSettings: () -> Void
@ -342,11 +365,14 @@ final class ChatMediaInputNodeInteraction {
var stickerSettings: ChatInterfaceStickerSettings? var stickerSettings: ChatInterfaceStickerSettings?
var highlightedStickerItemCollectionId: ItemCollectionId? var highlightedStickerItemCollectionId: ItemCollectionId?
var highlightedItemCollectionId: ItemCollectionId? var highlightedItemCollectionId: ItemCollectionId?
var highlightedGifMode: ChatMediaInputGifMode = .recent
var previewedStickerPackItem: StickerPreviewPeekItem? var previewedStickerPackItem: StickerPreviewPeekItem?
var appearanceTransition: CGFloat = 1.0 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.navigateToCollectionId = navigateToCollectionId
self.navigateBackToStickers = navigateBackToStickers
self.setGifMode = setGifMode
self.openSettings = openSettings self.openSettings = openSettings
self.toggleSearch = toggleSearch self.toggleSearch = toggleSearch
self.openPeerSpecificSettings = openPeerSpecificSettings self.openPeerSpecificSettings = openPeerSpecificSettings
@ -413,6 +439,7 @@ final class ChatMediaInputNode: ChatInputNode {
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
private let listView: ListView private let listView: ListView
private let gifListView: ListView
private var searchContainerNode: PaneSearchContainerNode? private var searchContainerNode: PaneSearchContainerNode?
private let searchContainerNodeLoadedDisposable = MetaDisposable() private let searchContainerNodeLoadedDisposable = MetaDisposable()
@ -475,9 +502,12 @@ final class ChatMediaInputNode: ChatInputNode {
self.listView = ListView() self.listView = ListView()
self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) 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 paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)?
var fixPaneScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState) -> 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 self.stickerPane = ChatMediaInputStickerPane(theme: theme, strings: strings, paneDidScroll: { pane, state, transition in
paneDidScrollImpl?(pane, state, transition) paneDidScrollImpl?(pane, state, transition)
@ -488,8 +518,8 @@ final class ChatMediaInputNode: ChatInputNode {
paneDidScrollImpl?(pane, state, transition) paneDidScrollImpl?(pane, state, transition)
}, fixPaneScroll: { pane, state in }, fixPaneScroll: { pane, state in
fixPaneScrollImpl?(pane, state) fixPaneScrollImpl?(pane, state)
}, openGifContextMenu: { fileReference, sourceNode, sourceRect, gesture, isSaved in }, openGifContextMenu: { file, sourceNode, sourceRect, gesture, isSaved in
openGifContextMenuImpl?(fileReference, sourceNode, sourceRect, gesture, isSaved) openGifContextMenuImpl?(file, sourceNode, sourceRect, gesture, isSaved)
}) })
var getItemIsPreviewedImpl: ((StickerPackItem) -> Bool)? 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 }, openSettings: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
let controller = installedStickerPacksController(context: context, mode: .modal) let controller = installedStickerPacksController(context: context, mode: .modal)
@ -563,8 +610,8 @@ final class ChatMediaInputNode: ChatInputNode {
self?.searchContainerNode?.deactivate() self?.searchContainerNode?.deactivate()
self?.inputNodeInteraction.toggleSearch(false, nil, "") self?.inputNodeInteraction.toggleSearch(false, nil, "")
}) })
searchContainerNode?.openGifContextMenu = { fileReference, sourceNode, sourceRect, gesture, isSaved in searchContainerNode?.openGifContextMenu = { file, sourceNode, sourceRect, gesture, isSaved in
self?.openGifContextMenu(fileReference: fileReference, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) self?.openGifContextMenu(file: file, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
} }
strongSelf.searchContainerNode = searchContainerNode strongSelf.searchContainerNode = searchContainerNode
if !query.isEmpty { if !query.isEmpty {
@ -644,6 +691,8 @@ final class ChatMediaInputNode: ChatInputNode {
self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor
self.collectionListPanel.addSubnode(self.listView) self.collectionListPanel.addSubnode(self.listView)
self.collectionListPanel.addSubnode(self.gifListView)
self.gifListView.isHidden = true
self.collectionListContainer.addSubnode(self.collectionListPanel) self.collectionListContainer.addSubnode(self.collectionListPanel)
self.collectionListContainer.addSubnode(self.collectionListSeparator) self.collectionListContainer.addSubnode(self.collectionListSeparator)
self.addSubnode(self.collectionListContainer) self.addSubnode(self.collectionListContainer)
@ -695,7 +744,7 @@ final class ChatMediaInputNode: ChatInputNode {
} }
self.inputNodeInteraction.stickerSettings = self.controllerInteraction.stickerSettings self.inputNodeInteraction.stickerSettings = self.controllerInteraction.stickerSettings
let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [])) let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [], []))
let inputNodeInteraction = self.inputNodeInteraction! let inputNodeInteraction = self.inputNodeInteraction!
let peerSpecificPack: Signal<(PeerSpecificPackData?, CanInstallPeerSpecificPack), NoError> let peerSpecificPack: Signal<(PeerSpecificPackData?, CanInstallPeerSpecificPack), NoError>
@ -779,7 +828,7 @@ final class ChatMediaInputNode: ChatInputNode {
let previousView = Atomic<ItemCollectionsView?>(value: nil) let previousView = Atomic<ItemCollectionsView?>(value: nil)
let transitionQueue = Queue() let transitionQueue = Queue()
let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get()) 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 (view, viewUpdate) = viewAndUpdate
let previous = previousView.swap(view) let previous = previousView.swap(view)
var update = viewUpdate 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 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) var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, strings: strings, theme: theme)
if view.higher == nil { if view.higher == nil {
@ -833,21 +883,19 @@ final class ChatMediaInputNode: ChatInputNode {
} }
} }
let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries)) let (previousPanelEntries, previousGifPaneEntries, previousGridEntries) = previousEntries.swap((panelEntries, gifPaneEntries, 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) 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 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 { if let strongSelf = self {
strongSelf.currentView = view strongSelf.currentView = view
strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime) strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime)
strongSelf.enqueueGifPanelTransition(gifPaneTransition, firstTime: false)
if !strongSelf.initializedArrangement { if !strongSelf.initializedArrangement {
strongSelf.initializedArrangement = true strongSelf.initializedArrangement = true
var currentPane = strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] let currentPane = strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex]
if view.entries.isEmpty {
//currentPane = .trending
}
if currentPane != strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] { if currentPane != strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] {
strongSelf.setCurrentPane(currentPane, transition: .immediate) strongSelf.setCurrentPane(currentPane, transition: .immediate)
} }
@ -869,7 +917,7 @@ final class ChatMediaInputNode: ChatInputNode {
} }
} }
if let collectionId = topVisibleCollectionId { if let collectionId = topVisibleCollectionId {
if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId { if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId && strongSelf.inputNodeInteraction.highlightedItemCollectionId?.namespace != ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue {
strongSelf.setHighlightedItemCollectionId(collectionId) strongSelf.setHighlightedItemCollectionId(collectionId)
} }
} }
@ -911,8 +959,8 @@ final class ChatMediaInputNode: ChatInputNode {
self?.fixPaneScroll(pane: pane, state: state) self?.fixPaneScroll(pane: pane, state: state)
} }
openGifContextMenuImpl = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in openGifContextMenuImpl = { [weak self] file, sourceNode, sourceRect, gesture, isSaved in
self?.openGifContextMenu(fileReference: fileReference, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) self?.openGifContextMenu(file: file, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
} }
} }
@ -921,9 +969,9 @@ final class ChatMediaInputNode: ChatInputNode {
self.searchContainerNodeLoadedDisposable.dispose() 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 let canSaveGif: Bool
if fileReference.media.fileId.namespace == Namespaces.Media.CloudFile { if file.file.media.fileId.namespace == Namespaces.Media.CloudFile {
canSaveGif = true canSaveGif = true
} else { } else {
canSaveGif = false canSaveGif = false
@ -933,14 +981,14 @@ final class ChatMediaInputNode: ChatInputNode {
if !canSaveGif { if !canSaveGif {
return false 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 |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in
guard let strongSelf = self else { guard let strongSelf = self else {
return 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 let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in
}, baseNavigationController: nil) }, baseNavigationController: nil)
@ -949,7 +997,11 @@ final class ChatMediaInputNode: ChatInputNode {
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.strings.MediaPicker_Send, icon: { _ in nil }, action: { _, f in items.append(.action(ContextMenuActionItem(text: strongSelf.strings.MediaPicker_Send, icon: { _ in nil }, action: { _, f in
f(.default) 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 { if isSaved || isGifSaved {
items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { _ in nil }, action: { _, f in 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 { guard let strongSelf = self else {
return 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 { } else if canSaveGif && !isGifSaved {
items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Preview_SaveGif, icon: { _ in nil }, action: { _, f in 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 { guard let strongSelf = self else {
return 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() super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true 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 { if let strongSelf = self {
let panes: [ASDisplayNode] let panes: [ASDisplayNode]
if let searchContainerNode = strongSelf.searchContainerNode { if let searchContainerNode = strongSelf.searchContainerNode {
@ -1071,23 +1123,8 @@ final class ChatMediaInputNode: ChatInputNode {
return nil return nil
} }
} }
} else if let file = item as? FileMediaReference { } else if let _ = item as? FileMediaReference {
return nil 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 { } else {
@ -1103,21 +1140,6 @@ final class ChatMediaInputNode: ChatInputNode {
if let pane = pane as? ChatMediaInputGifPane { if let pane = pane as? ChatMediaInputGifPane {
if let (_, _, _) = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) { if let (_, _, _) = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) {
return nil 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 { } else if pane is ChatMediaInputStickerPane || pane is ChatMediaInputTrendingPane {
var itemNodeAndItem: (ASDisplayNode, StickerPackItem)? var itemNodeAndItem: (ASDisplayNode, StickerPackItem)?
@ -1208,7 +1230,8 @@ final class ChatMediaInputNode: ChatInputNode {
} }
strongSelf.updatePreviewingItem(item: item, animated: true) strongSelf.updatePreviewingItem(item: item, animated: true)
} }
})) })
self.view.addGestureRecognizer(peekRecognizer)
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
self.panRecognizer = panRecognizer self.panRecognizer = panRecognizer
self.view.addGestureRecognizer(panRecognizer) self.view.addGestureRecognizer(panRecognizer)
@ -1283,6 +1306,31 @@ final class ChatMediaInputNode: ChatInputNode {
self.inputNodeInteraction.highlightedItemCollectionId = collectionId 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 ensuredNodeVisible = false
var firstVisibleCollectionId: ItemCollectionId? var firstVisibleCollectionId: ItemCollectionId?
self.listView.forEachItemNode { itemNode in self.listView.forEachItemNode { itemNode in
@ -1374,6 +1422,7 @@ final class ChatMediaInputNode: ChatInputNode {
self.inputNodeInteraction.appearanceTransition = max(0.1, value) self.inputNodeInteraction.appearanceTransition = max(0.1, value)
transition.updateAlpha(node: self.listView, alpha: value) transition.updateAlpha(node: self.listView, alpha: value)
transition.updateAlpha(node: self.gifListView, alpha: value)
self.listView.forEachItemNode { itemNode in self.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode { if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode {
itemNode.updateAppearanceTransition(transition: transition) itemNode.updateAppearanceTransition(transition: transition)
@ -1389,6 +1438,11 @@ final class ChatMediaInputNode: ChatInputNode {
itemNode.updateAppearanceTransition(transition: transition) itemNode.updateAppearanceTransition(transition: transition)
} }
} }
self.gifListView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode {
itemNode.updateAppearanceTransition(transition: transition)
}
}
} }
func simulateUpdateLayout(isVisible: Bool) { 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) 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)) 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 (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) 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.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 visiblePanes: [(ChatMediaInputPaneType, CGFloat)] = []
var paneIndex = 0 var paneIndex = 0
@ -1508,7 +1567,11 @@ final class ChatMediaInputNode: ChatInputNode {
case .gifs: case .gifs:
if self.gifPane.supernode == nil { if self.gifPane.supernode == nil {
if !displaySearch { if !displaySearch {
if let searchContainerNode = self.searchContainerNode {
self.insertSubnode(self.gifPane, belowSubnode: searchContainerNode)
} else {
self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer) self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer)
}
if self.searchContainerNode == nil { if self.searchContainerNode == nil {
self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 0.0), size: CGSize(width: width, height: panelHeight)) 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: case .stickers:
if self.stickerPane.supernode == nil { if self.stickerPane.supernode == nil {
if let searchContainerNode = self.searchContainerNode {
self.insertSubnode(self.stickerPane, belowSubnode: searchContainerNode)
} else {
self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer) self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer)
}
self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight)) self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight))
} }
if self.stickerPane.frame != paneFrame { if self.stickerPane.frame != paneFrame {
self.stickerPane.layer.removeAnimation(forKey: "position") self.stickerPane.layer.removeAnimation(forKey: "position")
transition.updateFrame(node: self.stickerPane, frame: paneFrame) 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) { private func enqueueGridTransition(_ transition: ChatMediaInputGridTransition, firstTime: Bool) {
var itemTransition: ContainedViewLayoutTransition = .immediate var itemTransition: ContainedViewLayoutTransition = .immediate
if transition.animated { 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.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.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.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) { 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.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.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.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? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
@ -1833,7 +1901,8 @@ final class ChatMediaInputNode: ChatInputNode {
return result 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 { static func setupPanelIconInsets(item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> UIEdgeInsets {

View File

@ -25,72 +25,10 @@ enum ChatMediaInputPanelEntryStableId: Hashable {
case peerSpecific case peerSpecific
case trending case trending
case settings case settings
case stickersMode
static func ==(lhs: ChatMediaInputPanelEntryStableId, rhs: ChatMediaInputPanelEntryStableId) -> Bool { case savedGifs
switch lhs { case trendingGifs
case .recentGifs: case gifEmotion(String)
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
}
}
} }
enum ChatMediaInputPanelEntry: Comparable, Identifiable { enum ChatMediaInputPanelEntry: Comparable, Identifiable {
@ -102,6 +40,11 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
case peerSpecific(theme: PresentationTheme, peer: Peer) case peerSpecific(theme: PresentationTheme, peer: Peer)
case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme) 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 { var stableId: ChatMediaInputPanelEntryStableId {
switch self { switch self {
case .recentGifs: case .recentGifs:
@ -118,6 +61,14 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
return .peerSpecific return .peerSpecific
case let .stickerPack(_, info, _, _): case let .stickerPack(_, info, _, _):
return .stickerPack(info.id.id) 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 { } else {
return false 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 { } else {
return lhsIndex <= rhsIndex return lhsIndex <= rhsIndex
} }
default:
return true
} }
case let .trending(elevated, _): case let .trending(elevated, _):
if elevated { if elevated {
@ -238,8 +215,37 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
return false return false
} }
} }
case .settings: case .stickersMode:
return false 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: { return ChatMediaInputStickerPackItem(account: context.account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, index: index, theme: theme, selected: {
inputNodeInteraction.navigateToCollectionId(info.id) 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))
})
} }
} }
} }

View File

@ -1089,8 +1089,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
if wideLayout { if wideLayout {
if let size = file.size { if let size = file.size {
let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))" let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))"
if file.isAnimated && (!automaticDownload || !automaticPlayback) { if file.isAnimated {
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: "\(gifTitle) " + sizeString, size: nil, muted: false, active: false) 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) { else if let duration = file.duration, !message.flags.contains(.Unsent) {
let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition)

View File

@ -195,7 +195,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in
self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame)
}, openMessageContextActions: { _, _, _, _ 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: { [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) self?.openUrl(url)
}, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {

View File

@ -71,7 +71,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in 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: { }, presentController: { _, _ in }, navigationController: {
return nil return nil
}, chatControllerNode: { }, chatControllerNode: {
@ -188,6 +188,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
} }
} }
} }
}, navigateBackToStickers: {
}, setGifMode: { _ in
}, openSettings: { [weak self] in }, openSettings: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
// let controller = installedStickerPacksController(context: context, mode: .modal) // 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) var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, strings: strings, theme: theme)
if view.higher == nil { if view.higher == nil {

View File

@ -272,6 +272,10 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
let inputNodeInteraction = ChatMediaInputNodeInteraction( let inputNodeInteraction = ChatMediaInputNodeInteraction(
navigateToCollectionId: { _ in navigateToCollectionId: { _ in
}, },
navigateBackToStickers: {
},
setGifMode: { _ in
},
openSettings: { openSettings: {
}, },
toggleSearch: { _, _, _ in toggleSearch: { _, _, _ in

View File

@ -11,9 +11,7 @@ import AccountContext
import WebSearchUI import WebSearchUI
import AppBundle import AppBundle
func paneGifSearchForQuery(account: Account, query: String, offset: String?, updateActivity: ((Bool) -> Void)?) -> Signal<([FileMediaReference], String?)?, NoError> { func paneGifSearchForQuery(account: Account, query: String, offset: String?, incompleteResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> {
let delayRequest = true
let contextBot = account.postbox.transaction { transaction -> String in let contextBot = account.postbox.transaction { transaction -> String in
let configuration = currentSearchBotsConfiguration(transaction: transaction) let configuration = currentSearchBotsConfiguration(transaction: transaction)
return configuration.gifBotUsername ?? "gif" return configuration.gifBotUsername ?? "gif"
@ -34,25 +32,13 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, upd
} }
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { 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 |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in return { _ in
return .contextRequestResult(user, results) 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> let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>
if delayRequest { if delayRequest {
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue()) maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
@ -60,15 +46,16 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, upd
maybeDelayedContextResults = results maybeDelayedContextResults = results
} }
return botResult |> then(maybeDelayedContextResults) return maybeDelayedContextResults
} else { } else {
return .single({ _ in return nil }) return .single({ _ in return nil })
} }
} }
return contextBot return contextBot
|> mapToSignal { result -> Signal<([FileMediaReference], String?)?, NoError> in |> mapToSignal { result -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> in
if let r = result(nil), case let .contextRequestResult(_, collection) = r, let results = collection?.results { if let r = result(nil), case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
var references: [FileMediaReference] = [] let results = collection.results
var references: [MultiplexedVideoNodeFile] = []
for result in results { for result in results {
switch result { switch result {
case let .externalReference(externalReference): 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: [])]) 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): case let .internalReference(internalReference):
if let file = internalReference.file { 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 { } else {
return .complete() return .complete()
} }
@ -145,7 +134,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
private var validLayout: CGSize? private var validLayout: CGSize?
private let trendingPromise: Promise<[FileMediaReference]?> private let trendingPromise: Promise<[MultiplexedVideoNodeFile]?>
private let searchDisposable = MetaDisposable() private let searchDisposable = MetaDisposable()
private let _ready = Promise<Void>() private let _ready = Promise<Void>()
@ -156,11 +145,11 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
var deactivateSearchBar: (() -> Void)? var deactivateSearchBar: (() -> Void)?
var updateActivity: ((Bool) -> Void)? var updateActivity: ((Bool) -> Void)?
var requestUpdateQuery: ((String) -> 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 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.context = context
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction
self.inputNodeInteraction = inputNodeInteraction self.inputNodeInteraction = inputNodeInteraction
@ -198,13 +187,13 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
self.hasInitialText = true self.hasInitialText = true
self.isLoadingNextResults = true self.isLoadingNextResults = true
let signal: Signal<([FileMediaReference], String?)?, NoError> let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError>
if !text.isEmpty { if !text.isEmpty {
signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: "", updateActivity: self.updateActivity) signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: "", updateActivity: self.updateActivity)
self.updateActivity?(true) self.updateActivity?(true)
} else { } else {
signal = self.trendingPromise.get() signal = self.trendingPromise.get()
|> map { items -> ([FileMediaReference], String?)? in |> map { items -> ([MultiplexedVideoNodeFile], String?)? in
if let items = items { if let items = items {
return (items, nil) return (items, nil)
} else { } else {
@ -226,7 +215,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
} else { } else {
strongSelf.nextOffset = nil 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.updateActivity?(false)
strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty
})) }))
@ -241,7 +230,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
} }
self.isLoadingNextResults = true 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) signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity)
self.searchDisposable.set((signal self.searchDisposable.set((signal
@ -251,12 +240,12 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
} }
var files = strongSelf.multiplexedNode?.files.trending ?? [] 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 { for item in result {
if currentIds.contains(item.media.fileId) { if currentIds.contains(item.file.media.fileId) {
continue continue
} }
currentIds.insert(item.media.fileId) currentIds.insert(item.file.media.fileId)
files.append(item) files.append(item)
} }
@ -266,7 +255,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
} else { } else {
strongSelf.nextOffset = nil 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 strongSelf.notFoundNode.isHidden = text.isEmpty || !files.isEmpty
})) }))
} }
@ -326,8 +315,12 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
self.addSubnode(multiplexedNode) self.addSubnode(multiplexedNode)
multiplexedNode.fileSelected = { [weak self] fileReference, sourceNode, sourceRect in multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in
let _ = self?.controllerInteraction.sendGif(fileReference, sourceNode, sourceRect) 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 multiplexedNode.fileContextMenu = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in

View File

@ -32,112 +32,39 @@ private final class VisibleVideoItem {
case trending(MediaId) case trending(MediaId)
} }
let id: Id let id: Id
let fileReference: FileMediaReference let file: MultiplexedVideoNodeFile
let frame: CGRect let frame: CGRect
init(fileReference: FileMediaReference, frame: CGRect, isTrending: Bool) { init(file: MultiplexedVideoNodeFile, frame: CGRect, isTrending: Bool) {
self.fileReference = fileReference self.file = file
self.frame = frame self.frame = frame
if isTrending { if isTrending {
self.id = .trending(fileReference.media.fileId) self.id = .trending(file.file.media.fileId)
} else { } 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 { final class MultiplexedVideoNodeFiles {
let saved: [FileMediaReference] let saved: [MultiplexedVideoNodeFile]
let trending: [FileMediaReference] let trending: [MultiplexedVideoNodeFile]
let isSearch: Bool
init(saved: [FileMediaReference], trending: [FileMediaReference]) { init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool) {
self.saved = saved self.saved = saved
self.trending = trending self.trending = trending
} self.isSearch = isSearch
}
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
} }
} }
@ -168,13 +95,19 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: []) { private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false)
didSet {
let startTime = CFAbsoluteTimeGetCurrent() func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) {
self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: true) self.files = files
print("MultiplexedVideoNode files updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
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 displayItems: [VisibleVideoItem] = []
private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:] private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:]
private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:] private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:]
@ -185,7 +118,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:] private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
private let savedTitleNode: ImmediateTextNode private let savedTitleNode: ImmediateTextNode
private let trendingHeaderNode: TrendingHeaderNode private let trendingTitleNode: ImmediateTextNode
private var displayLink: CADisplayLink! private var displayLink: CADisplayLink!
private var timeOffset = 0.0 private var timeOffset = 0.0
@ -193,8 +126,8 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
private let timebase: CMTimebase private let timebase: CMTimebase
var fileSelected: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)? var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)?
var fileContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
var enableVideoNodes = false var enableVideoNodes = false
init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
@ -215,21 +148,18 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
self.savedTitleNode = ImmediateTextNode() self.savedTitleNode = ImmediateTextNode()
self.savedTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_SavedSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) 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() super.init()
self.trendingHeaderNode.reactionSelected = { [weak self] reaction in
self?.reactionSelected?(reaction)
}
self.isOpaque = true self.isOpaque = true
self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.alwaysBounceVertical = true self.scrollNode.view.alwaysBounceVertical = true
self.scrollNode.addSubnode(self.savedTitleNode) self.scrollNode.addSubnode(self.savedTitleNode)
self.scrollNode.addSubnode(self.trendingHeaderNode) self.scrollNode.addSubnode(self.trendingTitleNode)
self.addSubnode(self.trackingNode) self.addSubnode(self.trackingNode)
self.addSubnode(self.contextContainerNode) self.addSubnode(self.contextContainerNode)
@ -300,7 +230,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
} }
self.contextContainerNode.customActivationProgress = { [weak self] progress, update in self.contextContainerNode.customActivationProgress = { [weak self] progress, update in
guard let strongSelf = self, let gestureLocation = gestureLocation else { guard let _ = self, let _ = gestureLocation else {
return return
} }
/*let minScale: CGFloat = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width /*let minScale: CGFloat = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width
@ -356,10 +286,14 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
private var ignoreDidScroll: Bool = false
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreDidScroll {
self.updateImmediatelyVisibleItems() self.updateImmediatelyVisibleItems()
self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height) self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height)
} }
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.didEndScrolling?() self.didEndScrolling?()
@ -407,15 +341,12 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
thumbnailLayer.frame = item.frame thumbnailLayer.frame = item.frame
} }
} else { } 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 thumbnailLayer.frame = item.frame
self.scrollNode.layer.addSublayer(thumbnailLayer) self.scrollNode.layer.addSublayer(thumbnailLayer)
self.visibleThumbnailLayers[item.id] = 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 { if item.frame.maxY < minVisibleY {
continue continue
} }
@ -434,7 +365,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
layerHolder.layer.frame = item.frame layerHolder.layer.frame = item.frame
self.scrollNode.layer.addSublayer(layerHolder.layer) 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.visibleLayers[item.id] = (manager, layerHolder)
self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
@ -490,11 +421,9 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
if !drawableSize.width.isZero { if !drawableSize.width.isZero {
var displayItems: [VisibleVideoItem] = [] var displayItems: [VisibleVideoItem] = []
let idealHeight = self.idealHeight
var verticalOffset: CGFloat = self.topInset var verticalOffset: CGFloat = self.topInset
func commitFilesSpans(files: [FileMediaReference], isTrending: Bool) { func commitFilesSpans(files: [MultiplexedVideoNodeFile], isTrending: Bool) {
var rowsCount = 0 var rowsCount = 0
var firstRowMax = 0; var firstRowMax = 0;
@ -512,7 +441,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
for a in 0 ..< itemsCount { for a in 0 ..< itemsCount {
var size: CGSize var size: CGSize
if let dimensions = files[a].media.dimensions { if let dimensions = files[a].file.media.dimensions {
size = dimensions.cgSize size = dimensions.cgSize
} else { } else {
size = CGSize(width: 100.0, height: 100.0) 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 { if itemsToRow[index] != nil && currentRowHorizontalOffset + itemSize.width >= drawableSize.width - 10.0 {
itemSize.width = max(itemSize.width, drawableSize.width - currentRowHorizontalOffset) 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 currentRowHorizontalOffset += itemSize.width + 1.0
if itemsToRow[index] != nil { if itemsToRow[index] != nil {
@ -598,104 +527,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
func commitFiles(files: [FileMediaReference], isTrending: Bool) { var hasContent = false
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
}
}
if !self.files.saved.isEmpty { if !self.files.saved.isEmpty {
self.savedTitleNode.isHidden = false self.savedTitleNode.isHidden = false
let leftInset: CGFloat = 10.0 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) self.savedTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: savedTitleSize)
verticalOffset += savedTitleSize.height + 5.0 verticalOffset += savedTitleSize.height + 5.0
commitFilesSpans(files: self.files.saved, isTrending: false) commitFilesSpans(files: self.files.saved, isTrending: false)
//commitFiles(files: self.files.saved, isTrending: false) hasContent = true
} else { } else {
self.savedTitleNode.isHidden = true self.savedTitleNode.isHidden = true
} }
if !self.files.trending.isEmpty { if !self.files.trending.isEmpty {
self.trendingHeaderNode.isHidden = false if self.files.isSearch {
let trendingHeight = self.trendingHeaderNode.update(theme: self.theme, strings: self.strings, width: drawableSize.width, sideInset: 0.0) self.trendingTitleNode.isHidden = true
self.trendingHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: drawableSize.width, height: trendingHeight))
verticalOffset += trendingHeight
commitFilesSpans(files: self.files.trending, isTrending: true)
//commitFiles(files: self.files.trending, isTrending: true)
} else { } else {
self.trendingHeaderNode.isHidden = true 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)
} else {
self.trendingTitleNode.isHidden = true
} }
let contentSize = CGSize(width: drawableSize.width, height: verticalOffset + self.bottomInset) let contentSize = CGSize(width: drawableSize.width, height: verticalOffset + self.bottomInset)
@ -748,19 +587,19 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
func frameForItem(_ id: MediaId) -> CGRect? { func frameForItem(_ id: MediaId) -> CGRect? {
for item in self.displayItems { for item in self.displayItems {
if item.fileReference.media.fileId == id { if item.file.file.media.fileId == id {
return item.frame return item.frame
} }
} }
return nil 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) let offsetPoint = point.offsetBy(dx: 0.0, dy: self.scrollNode.bounds.minY)
return self.offsetFileAt(point: offsetPoint) 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 { for item in self.displayItems {
if item.frame.contains(point) { if item.frame.contains(point) {
let isSaved: Bool let isSaved: Bool
@ -770,7 +609,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
case .trending: case .trending:
isSaved = false isSaved = false
} }
return (item.fileReference, item.frame, isSaved) return (item.file, item.frame, isSaved)
} }
} }
return nil return nil

View File

@ -74,6 +74,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
return false return false
}, sendGif: { _, _, _ in }, sendGif: { _, _, _ in
return false return false
}, sendBotContextResultAsGif: { _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _ in }, requestMessageActionCallback: { _, _, _ in
}, requestMessageActionUrlAuth: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in
}, activateSwitchInline: { _, _ in }, activateSwitchInline: { _, _ in

View File

@ -39,13 +39,13 @@ final class PaneSearchContainerNode: ASDisplayNode {
private var validLayout: CGSize? private var validLayout: CGSize?
var openGifContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
var ready: Signal<Void, NoError> { var ready: Signal<Void, NoError> {
return self.contentNode.ready 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.context = context
self.mode = mode self.mode = mode
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction

View File

@ -1479,6 +1479,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
return false return false
}, sendGif: { _, _, _ in }, sendGif: { _, _, _ in
return false return false
}, sendBotContextResultAsGif: { _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _ in }, requestMessageActionCallback: { _, _, _ in
}, requestMessageActionUrlAuth: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in
}, activateSwitchInline: { _, _ in }, activateSwitchInline: { _, _ in

View File

@ -325,6 +325,8 @@ public class PeerMediaCollectionController: TelegramBaseController {
return false return false
}, sendGif: { _, _, _ in }, sendGif: { _, _, _ in
return false return false
}, sendBotContextResultAsGif: { _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _ in }, requestMessageActionCallback: { _, _, _ in
}, requestMessageActionUrlAuth: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in
}, activateSwitchInline: { _, _ in }, activateSwitchInline: { _, _ in

View File

@ -1106,7 +1106,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
tapMessage?(message) tapMessage?(message)
}, clickThroughMessage: { }, clickThroughMessage: {
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: { }, presentController: { _, _ in }, navigationController: {
return nil return nil
}, chatControllerNode: { }, chatControllerNode: {

View File

@ -109,6 +109,7 @@ final class SoftwareVideoLayerFrameManager {
while index < self.frames.count { while index < self.frames.count {
if baseTimestamp + self.frames[index].position.seconds + self.frames[index].duration.seconds <= timestamp { if baseTimestamp + self.frames[index].position.seconds + self.frames[index].duration.seconds <= timestamp {
latestFrameIndex = index latestFrameIndex = index
//print("latestFrameIndex = \(index)")
} }
index += 1 index += 1
} }
@ -139,7 +140,7 @@ final class SoftwareVideoLayerFrameManager {
private var polling = false private var polling = false
private func poll() { private func poll() {
if self.frames.count < 3 && !self.polling { if self.frames.count < 2 && !self.polling {
self.polling = true self.polling = true
let minPts = self.minPts let minPts = self.minPts
let maxPts = self.maxPts let maxPts = self.maxPts
@ -179,7 +180,11 @@ final class SoftwareVideoLayerFrameManager {
} }
if let frame = frameAndLoop?.0 { if let frame = frameAndLoop?.0 {
if strongSelf.minPts == nil || CMTimeCompare(strongSelf.minPts!, frame.position) < 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.append(frame)
strongSelf.frames.sort(by: { lhs, rhs in strongSelf.frames.sort(by: { lhs, rhs in
@ -190,7 +195,7 @@ final class SoftwareVideoLayerFrameManager {
} }
}) })
//print("add frame at \(CMTimeGetSeconds(frame.position))") //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)") //print("frames: \(positions)")
} else { } else {
//print("not adding frames") //print("not adding frames")

View File

@ -10,8 +10,8 @@ import LegacyComponents
import TelegramUIPreferences import TelegramUIPreferences
import AccountContext import AccountContext
public func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, limit: Int = 60) -> Signal<ChatContextResultCollection?, NoError> { public func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, limit: Int = 60) -> Signal<ChatContextResultCollection?, NoError> {
return requestChatContextResults(account: account, botId: botId, peerId: peerId, query: query, offset: offset) return requestChatContextResults(account: account, botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults)
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in |> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
return .single(nil) return .single(nil)
} }