mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
GIF-related fixes part 2
This commit is contained in:
parent
03e773580d
commit
c4dd56b596
@ -4,6 +4,13 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FFMpegAVCodecContextReceiveResult)
|
||||
{
|
||||
FFMpegAVCodecContextReceiveResultError,
|
||||
FFMpegAVCodecContextReceiveResultNotEnoughData,
|
||||
FFMpegAVCodecContextReceiveResultSuccess,
|
||||
};
|
||||
|
||||
@class FFMpegAVCodec;
|
||||
@class FFMpegAVFrame;
|
||||
|
||||
@ -17,7 +24,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (FFMpegAVSampleFormat)sampleFormat;
|
||||
|
||||
- (bool)open;
|
||||
- (bool)receiveIntoFrame:(FFMpegAVFrame *)frame;
|
||||
- (bool)sendEnd;
|
||||
- (FFMpegAVCodecContextReceiveResult)receiveIntoFrame:(FFMpegAVFrame *)frame;
|
||||
- (void)flushBuffers;
|
||||
|
||||
@end
|
||||
|
@ -14,6 +14,7 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFrameColorRange) {
|
||||
@property (nonatomic, readonly) uint8_t **data;
|
||||
@property (nonatomic, readonly) int *lineSize;
|
||||
@property (nonatomic, readonly) int64_t pts;
|
||||
@property (nonatomic, readonly) int64_t duration;
|
||||
@property (nonatomic, readonly) FFMpegAVFrameColorRange colorRange;
|
||||
|
||||
- (instancetype)init;
|
||||
|
@ -50,11 +50,22 @@
|
||||
return result >= 0;
|
||||
}
|
||||
|
||||
- (bool)receiveIntoFrame:(FFMpegAVFrame *)frame {
|
||||
int status = avcodec_receive_frame(_impl, (AVFrame *)[frame impl]);
|
||||
- (bool)sendEnd {
|
||||
int status = avcodec_send_packet(_impl, nil);
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
- (FFMpegAVCodecContextReceiveResult)receiveIntoFrame:(FFMpegAVFrame *)frame {
|
||||
int status = avcodec_receive_frame(_impl, (AVFrame *)[frame impl]);
|
||||
if (status == 0) {
|
||||
return FFMpegAVCodecContextReceiveResultSuccess;
|
||||
} else if (status == -35) {
|
||||
return FFMpegAVCodecContextReceiveResultNotEnoughData;
|
||||
} else {
|
||||
return FFMpegAVCodecContextReceiveResultError;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)flushBuffers {
|
||||
avcodec_flush_buffers(_impl);
|
||||
}
|
||||
|
@ -44,6 +44,10 @@
|
||||
return _impl->pts;
|
||||
}
|
||||
|
||||
- (int64_t)duration {
|
||||
return _impl->pkt_duration;
|
||||
}
|
||||
|
||||
- (FFMpegAVFrameColorRange)colorRange {
|
||||
switch (_impl->color_range) {
|
||||
case AVCOL_RANGE_MPEG:
|
||||
|
@ -21,9 +21,14 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
|
||||
func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? {
|
||||
let status = frame.packet.send(toDecoder: self.codecContext)
|
||||
if status == 0 {
|
||||
while self.codecContext.receive(into: self.audioFrame) {
|
||||
if let convertedFrame = convertAudioFrame(self.audioFrame, pts: frame.pts, duration: frame.duration) {
|
||||
self.delayedFrames.append(convertedFrame)
|
||||
while true {
|
||||
let result = self.codecContext.receive(into: self.audioFrame)
|
||||
if case .success = result {
|
||||
if let convertedFrame = convertAudioFrame(self.audioFrame, pts: frame.pts, duration: frame.duration) {
|
||||
self.delayedFrames.append(convertedFrame)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,11 +17,20 @@ private let deviceColorSpace: CGColorSpace = {
|
||||
}()
|
||||
|
||||
public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
||||
public enum ReceiveResult {
|
||||
case error
|
||||
case moreDataNeeded
|
||||
case result(MediaTrackFrame)
|
||||
}
|
||||
|
||||
private let codecContext: FFMpegAVCodecContext
|
||||
|
||||
private let videoFrame: FFMpegAVFrame
|
||||
private var resetDecoderOnNextFrame = true
|
||||
|
||||
private var defaultDuration: CMTime?
|
||||
private var defaultTimescale: CMTimeScale?
|
||||
|
||||
private var pixelBufferPool: CVPixelBufferPool?
|
||||
|
||||
private var delayedFrames: [MediaTrackFrame] = []
|
||||
@ -60,10 +69,51 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
||||
return self.decode(frame: frame, ptsOffset: nil)
|
||||
}
|
||||
|
||||
public func sendToDecoder(frame: MediaTrackDecodableFrame) -> Bool {
|
||||
self.defaultDuration = frame.duration
|
||||
self.defaultTimescale = frame.pts.timescale
|
||||
|
||||
let status = frame.packet.send(toDecoder: self.codecContext)
|
||||
return status == 0
|
||||
}
|
||||
|
||||
public func sendEndToDecoder() -> Bool {
|
||||
return self.codecContext.sendEnd()
|
||||
}
|
||||
|
||||
public func receiveFromDecoder(ptsOffset: CMTime?) -> ReceiveResult {
|
||||
guard let defaultTimescale = self.defaultTimescale, let defaultDuration = self.defaultDuration else {
|
||||
return .error
|
||||
}
|
||||
|
||||
let receiveResult = self.codecContext.receive(into: self.videoFrame)
|
||||
switch receiveResult {
|
||||
case .success:
|
||||
var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale)
|
||||
if let ptsOffset = ptsOffset {
|
||||
pts = CMTimeAdd(pts, ptsOffset)
|
||||
}
|
||||
if let convertedFrame = convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: self.videoFrame.duration > 0 ? CMTimeMake(value: self.videoFrame.duration, timescale: defaultTimescale) : defaultDuration) {
|
||||
return .result(convertedFrame)
|
||||
} else {
|
||||
return .error
|
||||
}
|
||||
case .notEnoughData:
|
||||
return .moreDataNeeded
|
||||
case .error:
|
||||
return .error
|
||||
@unknown default:
|
||||
return .error
|
||||
}
|
||||
}
|
||||
|
||||
public func decode(frame: MediaTrackDecodableFrame, ptsOffset: CMTime?) -> MediaTrackFrame? {
|
||||
let status = frame.packet.send(toDecoder: self.codecContext)
|
||||
if status == 0 {
|
||||
if self.codecContext.receive(into: self.videoFrame) {
|
||||
self.defaultDuration = frame.duration
|
||||
self.defaultTimescale = frame.pts.timescale
|
||||
|
||||
if self.codecContext.receive(into: self.videoFrame) == .success {
|
||||
var pts = CMTimeMake(value: self.videoFrame.pts, timescale: frame.pts.timescale)
|
||||
if let ptsOffset = ptsOffset {
|
||||
pts = CMTimeAdd(pts, ptsOffset)
|
||||
@ -75,10 +125,35 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func receiveRemainingFrames(ptsOffset: CMTime?) -> [MediaTrackFrame] {
|
||||
guard let defaultTimescale = self.defaultTimescale, let defaultDuration = self.defaultDuration else {
|
||||
return []
|
||||
}
|
||||
|
||||
var result: [MediaTrackFrame] = []
|
||||
result.append(contentsOf: self.delayedFrames)
|
||||
self.delayedFrames.removeAll()
|
||||
|
||||
while true {
|
||||
if case .success = self.codecContext.receive(into: self.videoFrame) {
|
||||
var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale)
|
||||
if let ptsOffset = ptsOffset {
|
||||
pts = CMTimeAdd(pts, ptsOffset)
|
||||
}
|
||||
if let convertedFrame = convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: self.videoFrame.duration > 0 ? CMTimeMake(value: self.videoFrame.duration, timescale: defaultTimescale) : defaultDuration) {
|
||||
result.append(convertedFrame)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func render(frame: MediaTrackDecodableFrame) -> UIImage? {
|
||||
let status = frame.packet.send(toDecoder: self.codecContext)
|
||||
if status == 0 {
|
||||
if self.codecContext.receive(into: self.videoFrame) {
|
||||
if case .success = self.codecContext.receive(into: self.videoFrame) {
|
||||
return convertVideoFrameToImage(self.videoFrame)
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,9 @@ public final class SoftwareVideoSource {
|
||||
fileprivate let fd: Int32?
|
||||
fileprivate let size: Int32
|
||||
|
||||
private var enqueuedFrames: [(MediaTrackFrame, CGFloat, CGFloat, Bool)] = []
|
||||
private var hasReadToEnd: Bool = false
|
||||
|
||||
public init(path: String) {
|
||||
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
|
||||
|
||||
@ -178,7 +181,7 @@ public final class SoftwareVideoSource {
|
||||
} else {
|
||||
if let avFormatContext = self.avFormatContext, let videoStream = self.videoStream {
|
||||
endOfStream = true
|
||||
avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true)
|
||||
break
|
||||
} else {
|
||||
endOfStream = true
|
||||
break
|
||||
@ -187,30 +190,44 @@ public final class SoftwareVideoSource {
|
||||
}
|
||||
}
|
||||
|
||||
if endOfStream {
|
||||
if let videoStream = self.videoStream {
|
||||
videoStream.decoder.reset()
|
||||
}
|
||||
}
|
||||
|
||||
return (frames.first, endOfStream)
|
||||
}
|
||||
|
||||
public func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) {
|
||||
if let videoStream = self.videoStream {
|
||||
let (decodableFrame, loop) = self.readDecodableFrame()
|
||||
if let decodableFrame = decodableFrame {
|
||||
var ptsOffset: CMTime?
|
||||
if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 {
|
||||
ptsOffset = maxPts
|
||||
}
|
||||
return (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
} else {
|
||||
return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
}
|
||||
} else {
|
||||
guard let videoStream = self.videoStream, let avFormatContext = self.avFormatContext else {
|
||||
return (nil, 0.0, 1.0, false)
|
||||
}
|
||||
|
||||
if !self.enqueuedFrames.isEmpty {
|
||||
let value = self.enqueuedFrames.removeFirst()
|
||||
return (value.0, value.1, value.2, value.3)
|
||||
}
|
||||
|
||||
let (decodableFrame, loop) = self.readDecodableFrame()
|
||||
var result: (MediaTrackFrame?, CGFloat, CGFloat, Bool)
|
||||
if let decodableFrame = decodableFrame {
|
||||
var ptsOffset: CMTime?
|
||||
if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 {
|
||||
ptsOffset = maxPts
|
||||
}
|
||||
result = (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
} else {
|
||||
result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
}
|
||||
if loop {
|
||||
let _ = videoStream.decoder.sendEndToDecoder()
|
||||
let remainingFrames = videoStream.decoder.receiveRemainingFrames(ptsOffset: maxPts)
|
||||
for i in 0 ..< remainingFrames.count {
|
||||
self.enqueuedFrames.append((remainingFrames[i], CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), i == remainingFrames.count - 1))
|
||||
}
|
||||
videoStream.decoder.reset()
|
||||
avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true)
|
||||
|
||||
if result.0 == nil && !self.enqueuedFrames.isEmpty {
|
||||
result = self.enqueuedFrames.removeFirst()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func readImage() -> (UIImage?, CGFloat, CGFloat, Bool) {
|
||||
|
@ -130,11 +130,26 @@ final class MessageHistoryHoleIndexTable: Table {
|
||||
if !self.metadataTable.isInitialized(peerId) {
|
||||
self.metadataTable.setInitialized(peerId)
|
||||
if let tagsByNamespace = self.seedConfiguration.messageHoles[peerId.namespace] {
|
||||
for (namespace, _) in tagsByNamespace {
|
||||
for (namespace, tags) in tagsByNamespace {
|
||||
for tag in tags {
|
||||
self.metadataTable.setPeerTagInitialized(peerId: peerId, tag: tag)
|
||||
}
|
||||
var operations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:]
|
||||
self.add(peerId: peerId, namespace: namespace, space: .everywhere, range: 1 ... (Int32.max - 1), operations: &operations)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let tagsByNamespace = self.seedConfiguration.upgradedMessageHoles[peerId.namespace] {
|
||||
for (namespace, tags) in tagsByNamespace {
|
||||
for tag in tags {
|
||||
if !self.metadataTable.isPeerTagInitialized(peerId: peerId, tag: tag) {
|
||||
self.metadataTable.setPeerTagInitialized(peerId: peerId, tag: tag)
|
||||
var operations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:]
|
||||
self.add(peerId: peerId, namespace: namespace, space: .tag(tag), range: 1 ... (Int32.max - 1), operations: &operations)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ private enum MetadataPrefix: Int8 {
|
||||
case PeerHistoryInitialized = 9
|
||||
case ShouldReindexUnreadCountsState = 10
|
||||
case TotalUnreadCountStates = 11
|
||||
case PeerHistoryTagInitialized = 12
|
||||
}
|
||||
|
||||
public struct ChatListTotalUnreadCounters: PostboxCoding, Equatable {
|
||||
@ -51,6 +52,7 @@ final class MessageHistoryMetadataTable: Table {
|
||||
|
||||
private var initializedChatList = Set<InitializedChatListKey>()
|
||||
private var initializedHistoryPeerIds = Set<PeerId>()
|
||||
private var initializedHistoryPeerIdTags: [PeerId: Set<MessageTags>] = [:]
|
||||
private var initializedGroupFeedIndexIds = Set<PeerGroupId>()
|
||||
|
||||
private var peerNextMessageIdByNamespace: [PeerId: [MessageId.Namespace: MessageId.Id]] = [:]
|
||||
@ -74,6 +76,14 @@ final class MessageHistoryMetadataTable: Table {
|
||||
return self.sharedPeerHistoryInitializedKey
|
||||
}
|
||||
|
||||
private func peerHistoryInitializedTagKey(id: PeerId, tag: UInt32) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8 + 1 + 4)
|
||||
key.setInt64(0, value: id.toInt64())
|
||||
key.setInt8(8, value: MetadataPrefix.PeerHistoryTagInitialized.rawValue)
|
||||
key.setUInt32(8 + 1, value: tag)
|
||||
return key
|
||||
}
|
||||
|
||||
private func groupFeedIndexInitializedKey(_ id: PeerGroupId) -> ValueBoxKey {
|
||||
self.sharedGroupFeedIndexInitializedKey.setInt32(0, value: id.rawValue)
|
||||
self.sharedGroupFeedIndexInitializedKey.setInt8(4, value: MetadataPrefix.GroupFeedIndexInitialized.rawValue)
|
||||
@ -201,6 +211,31 @@ final class MessageHistoryMetadataTable: Table {
|
||||
}
|
||||
}
|
||||
|
||||
func setPeerTagInitialized(peerId: PeerId, tag: MessageTags) {
|
||||
if self.initializedHistoryPeerIdTags[peerId] == nil {
|
||||
self.initializedHistoryPeerIdTags[peerId] = Set()
|
||||
}
|
||||
initializedHistoryPeerIdTags[peerId]!.insert(tag)
|
||||
self.sharedBuffer.reset()
|
||||
self.valueBox.set(self.table, key: self.peerHistoryInitializedTagKey(id: peerId, tag: tag.rawValue), value: self.sharedBuffer)
|
||||
}
|
||||
|
||||
func isPeerTagInitialized(peerId: PeerId, tag: MessageTags) -> Bool {
|
||||
if let currentTags = self.initializedHistoryPeerIdTags[peerId], currentTags.contains(tag) {
|
||||
return true
|
||||
} else {
|
||||
if self.valueBox.exists(self.table, key: self.peerHistoryInitializedTagKey(id: peerId, tag: tag.rawValue)) {
|
||||
if self.initializedHistoryPeerIdTags[peerId] == nil {
|
||||
self.initializedHistoryPeerIdTags[peerId] = Set()
|
||||
}
|
||||
initializedHistoryPeerIdTags[peerId]!.insert(tag)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setGroupFeedIndexInitialized(_ groupId: PeerGroupId) {
|
||||
self.initializedGroupFeedIndexIds.insert(groupId)
|
||||
self.sharedBuffer.reset()
|
||||
|
@ -59,6 +59,7 @@ public final class SeedConfiguration {
|
||||
public let globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>
|
||||
public let initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?)
|
||||
public let messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]]
|
||||
public let upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]]
|
||||
public let messageTagsWithSummary: MessageTags
|
||||
public let existingGlobalMessageTags: GlobalMessageTags
|
||||
public let peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace]
|
||||
@ -70,10 +71,11 @@ public final class SeedConfiguration {
|
||||
public let globalNotificationSettingsPreferencesKey: ValueBoxKey
|
||||
public let defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings
|
||||
|
||||
public init(globalMessageIdsPeerIdNamespaces: Set<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.initializeChatListWithHole = initializeChatListWithHole
|
||||
self.messageHoles = messageHoles
|
||||
self.upgradedMessageHoles = upgradedMessageHoles
|
||||
self.messageTagsWithSummary = messageTagsWithSummary
|
||||
self.existingGlobalMessageTags = existingGlobalMessageTags
|
||||
self.peerNamespacesRequiringMessageTextIndex = peerNamespacesRequiringMessageTextIndex
|
||||
|
@ -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>()
|
||||
for peerIdNamespace in [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup] {
|
||||
globalMessageIdsPeerIdNamespaces.insert(GlobalMessageIdsNamespace(peerIdNamespace: peerIdNamespace, messageIdNamespace: Namespaces.Message.Cloud))
|
||||
}
|
||||
|
||||
return SeedConfiguration(globalMessageIdsPeerIdNamespaces: globalMessageIdsPeerIdNamespaces, initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1))), messageHoles: messageHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: MessageTags.unseenPersonalMessage, existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer, isContact in
|
||||
return SeedConfiguration(globalMessageIdsPeerIdNamespaces: globalMessageIdsPeerIdNamespaces, initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1))), messageHoles: messageHoles, upgradedMessageHoles: upgradedMessageHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: MessageTags.unseenPersonalMessage, existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer, isContact in
|
||||
if let peer = peer as? TelegramUser {
|
||||
if peer.botInfo != nil {
|
||||
return .bot
|
||||
|
@ -41,7 +41,7 @@ private struct RequestData: Codable {
|
||||
|
||||
private let requestVersion = "3"
|
||||
|
||||
public func requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String) -> Signal<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
|
||||
if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) {
|
||||
return (bot, peer)
|
||||
@ -92,7 +92,9 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P
|
||||
flags |= (1 << 0)
|
||||
geoPoint = Api.InputGeoPoint.inputGeoPoint(lat: latitude, long: longitude)
|
||||
}
|
||||
return account.network.request(Api.functions.messages.getInlineBotResults(flags: flags, bot: inputBot, peer: inputPeer, geoPoint: geoPoint, query: query, offset: offset))
|
||||
|
||||
|
||||
var signal: Signal<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
|
||||
return ChatContextResultCollection(apiResults: result, botId: bot.id, peerId: peerId, query: query, geoPoint: location)
|
||||
}
|
||||
@ -122,6 +124,12 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P
|
||||
}
|
||||
|> castError(RequestChatContextResultsError.self)
|
||||
}
|
||||
|
||||
if incompleteResults {
|
||||
signal = .single(nil) |> then(signal)
|
||||
}
|
||||
|
||||
return signal
|
||||
}
|
||||
|> castError(RequestChatContextResultsError.self)
|
||||
|> switchToLatest
|
||||
|
@ -130,6 +130,8 @@ public enum PresentationResourceKey: Int32 {
|
||||
case chatInputMediaPanelAddedPackButtonImage
|
||||
case chatInputMediaPanelGridSetupImage
|
||||
case chatInputMediaPanelGridDismissImage
|
||||
case chatInputMediaPanelTrendingGifsIcon
|
||||
case chatInputMediaPanelStickersModeIcon
|
||||
|
||||
case chatInputButtonPanelButtonImage
|
||||
case chatInputButtonPanelButtonHighlightedImage
|
||||
|
@ -214,6 +214,28 @@ public struct PresentationResourcesChat {
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatInputMediaPanelStickersModeIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatInputMediaPanelStickersModeIcon.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/StickersMode"), color: theme.chat.inputMediaPanel.panelIconColor) {
|
||||
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatInputMediaPanelTrendingGifsIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatInputMediaPanelTrendingGifsIcon.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/TrendingGifs"), color: theme.chat.inputMediaPanel.panelIconColor) {
|
||||
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatInputMediaPanelRecentStickersIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatInputMediaPanelRecentStickersIconImage.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
|
||||
|
@ -1,22 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StickerKeyboardGifIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StickerKeyboardGifIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_input_gifs.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB |
BIN
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/ic_input_gifs.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/ic_input_gifs.pdf
vendored
Normal file
Binary file not shown.
@ -1,22 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StickerKeyboardRecentTab@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StickerKeyboardRecentTab@3x.png",
|
||||
"scale" : "3x"
|
||||
"filename" : "ic_input_recent.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 696 B |
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_input_stickers.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/ic_input_stickers.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/StickersMode.imageset/ic_input_stickers.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_input_trending.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/ic_input_trending.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingGifs.imageset/ic_input_trending.pdf
vendored
Normal file
Binary file not shown.
@ -1,12 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_addstickers.pdf"
|
||||
"filename" : "ic_input_addstickers.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
@ -766,6 +766,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
})
|
||||
strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)])
|
||||
}
|
||||
return true
|
||||
}, sendBotContextResultAsGif: { [weak self] collection, result, sourceNode, sourceRect in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
if let _ = strongSelf.presentationInterfaceState.slowmodeState, !strongSelf.presentationInterfaceState.isScheduledMessages {
|
||||
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect)
|
||||
return false
|
||||
}
|
||||
|
||||
strongSelf.enqueueChatContextResult(collection, result, hideVia: true, closeMediaInput: true)
|
||||
|
||||
return true
|
||||
}, requestMessageActionCallback: { [weak self] messageId, data, isGame in
|
||||
if let strongSelf = self {
|
||||
@ -6946,16 +6958,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}))
|
||||
}
|
||||
|
||||
private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false) {
|
||||
private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false) {
|
||||
guard case let .peer(peerId) = self.chatLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
if let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result, hideVia: hideVia), canSendMessagesToChat(self.presentationInterfaceState) {
|
||||
let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId
|
||||
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) }
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||||
var state = state
|
||||
state = state.updatedInterfaceState { interfaceState in
|
||||
var interfaceState = interfaceState
|
||||
interfaceState = interfaceState.withUpdatedReplyMessageId(nil)
|
||||
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "")))
|
||||
interfaceState = interfaceState.withUpdatedComposeDisableUrlPreview(nil)
|
||||
return interfaceState
|
||||
}
|
||||
state = state.updatedInputMode { current in
|
||||
if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil {
|
||||
return .media(mode: mode, expanded: nil)
|
||||
}
|
||||
return current
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -70,6 +70,7 @@ public final class ChatControllerInteraction {
|
||||
let sendMessage: (String) -> Void
|
||||
let sendSticker: (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool
|
||||
let sendGif: (FileMediaReference, ASDisplayNode, CGRect) -> Bool
|
||||
let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool
|
||||
let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void
|
||||
let requestMessageActionUrlAuth: (String, MessageId, Int32) -> Void
|
||||
let activateSwitchInline: (PeerId?, String) -> Void
|
||||
@ -135,7 +136,7 @@ public final class ChatControllerInteraction {
|
||||
var searchTextHighightState: (String, [MessageIndex])?
|
||||
var seenOneTimeAnimatedMedia = Set<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.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
@ -149,6 +150,7 @@ public final class ChatControllerInteraction {
|
||||
self.sendMessage = sendMessage
|
||||
self.sendSticker = sendSticker
|
||||
self.sendGif = sendGif
|
||||
self.sendBotContextResultAsGif = sendBotContextResultAsGif
|
||||
self.requestMessageActionCallback = requestMessageActionCallback
|
||||
self.requestMessageActionUrlAuth = requestMessageActionUrlAuth
|
||||
self.activateSwitchInline = activateSwitchInline
|
||||
@ -209,7 +211,7 @@ public final class ChatControllerInteraction {
|
||||
|
||||
static var `default`: ChatControllerInteraction {
|
||||
return ChatControllerInteraction(openMessage: { _, _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
return nil
|
||||
}, chatControllerNode: {
|
||||
|
@ -32,7 +32,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
|
||||
private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void
|
||||
private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void
|
||||
private let openGifContextMenu: (FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void
|
||||
private let openGifContextMenu: (MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void
|
||||
|
||||
private let searchPlaceholderNode: PaneSearchBarPlaceholderNode
|
||||
var visibleSearchPlaceholderNode: PaneSearchBarPlaceholderNode? {
|
||||
@ -49,14 +49,16 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
private let emptyNode: ImmediateTextNode
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
let trendingPromise = Promise<[FileMediaReference]?>(nil)
|
||||
let trendingPromise = Promise<[MultiplexedVideoNodeFile]?>(nil)
|
||||
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat, Bool, Bool, DeviceMetrics)?
|
||||
private var didScrollPreviousOffset: CGFloat?
|
||||
|
||||
private var didScrollPreviousState: ChatMediaInputPaneScrollState?
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void) {
|
||||
private(set) var mode: ChatMediaInputGifMode = .recent
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@ -72,6 +74,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
self.emptyNode.attributedText = NSAttributedString(string: strings.Gif_NoGifsPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
self.emptyNode.textAlignment = .center
|
||||
self.emptyNode.maximumNumberOfLines = 3
|
||||
self.emptyNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
@ -116,7 +119,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
self.updateMultiplexedNodeLayout(changedIsExpanded: changedIsExpanded, transition: transition)
|
||||
}
|
||||
|
||||
func fileAt(point: CGPoint) -> (FileMediaReference, CGRect, Bool)? {
|
||||
func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? {
|
||||
if let multiplexedNode = self.multiplexedNode {
|
||||
return multiplexedNode.fileAt(point: point.offsetBy(dx: -multiplexedNode.frame.minX, dy: -multiplexedNode.frame.minY))
|
||||
} else {
|
||||
@ -124,6 +127,14 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func setMode(mode: ChatMediaInputGifMode) {
|
||||
if self.mode == mode {
|
||||
return
|
||||
}
|
||||
self.mode = mode
|
||||
self.resetMode(synchronous: true)
|
||||
}
|
||||
|
||||
override var isEmpty: Bool {
|
||||
if let files = self.multiplexedNode?.files {
|
||||
return files.trending.isEmpty && files.saved.isEmpty
|
||||
@ -139,13 +150,23 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
private func updateMultiplexedNodeLayout(changedIsExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
||||
guard let (size, topInset, bottomInset, isExpanded, _, deviceMetrics) = self.validLayout else {
|
||||
guard let (size, topInset, bottomInset, _, _, deviceMetrics) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
if let multiplexedNode = self.multiplexedNode {
|
||||
let previousBounds = multiplexedNode.scrollNode.layer.bounds
|
||||
multiplexedNode.topInset = topInset + 60.0
|
||||
let _ = multiplexedNode.scrollNode.layer.bounds
|
||||
|
||||
let displaySearch: Bool
|
||||
|
||||
switch self.mode {
|
||||
case .recent:
|
||||
displaySearch = true
|
||||
default:
|
||||
displaySearch = false
|
||||
}
|
||||
|
||||
multiplexedNode.topInset = topInset + (displaySearch ? 60.0 : 0.0)
|
||||
multiplexedNode.bottomInset = bottomInset
|
||||
|
||||
if case .tablet = deviceMetrics.type, size.width > 480.0 {
|
||||
@ -156,11 +177,11 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
|
||||
var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size)
|
||||
/*var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size)
|
||||
if changedIsExpanded {
|
||||
let isEmpty = multiplexedNode.files.trending.isEmpty && multiplexedNode.files.saved.isEmpty
|
||||
//targetBounds.origin.y = isExpanded || isEmpty ? 0.0 : 60.0
|
||||
}
|
||||
}*/
|
||||
|
||||
//transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: targetBounds)
|
||||
transition.updateFrame(node: multiplexedNode, frame: nodeFrame)
|
||||
@ -172,8 +193,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
|
||||
func initializeIfNeeded() {
|
||||
if self.multiplexedNode == nil {
|
||||
self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, updateActivity: nil)
|
||||
|> map { items -> [FileMediaReference]? in
|
||||
self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil)
|
||||
|> map { items -> [MultiplexedVideoNodeFile]? in
|
||||
if let (items, _) = items {
|
||||
return items
|
||||
} else {
|
||||
@ -197,47 +218,12 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
self.addSubnode(multiplexedNode)
|
||||
multiplexedNode.scrollNode.addSubnode(self.searchPlaceholderNode)
|
||||
|
||||
let gifs = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]))
|
||||
|> map { trending, view -> MultiplexedVideoNodeFiles in
|
||||
var recentGifs: OrderedItemListView?
|
||||
if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] {
|
||||
recentGifs = orderedView as? OrderedItemListView
|
||||
}
|
||||
|
||||
var saved: [FileMediaReference] = []
|
||||
|
||||
if let recentGifs = recentGifs {
|
||||
saved = recentGifs.items.map { item in
|
||||
let file = (item.contents as! RecentMediaItem).media as! TelegramMediaFile
|
||||
return .savedGif(media: file)
|
||||
}
|
||||
multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in
|
||||
if let (collection, result) = file.contextResult {
|
||||
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect)
|
||||
} else {
|
||||
saved = []
|
||||
let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect)
|
||||
}
|
||||
|
||||
return MultiplexedVideoNodeFiles(saved: saved, trending: trending ?? [])
|
||||
}
|
||||
self.disposable.set((gifs
|
||||
|> deliverOnMainQueue).start(next: { [weak self] files in
|
||||
if let strongSelf = self {
|
||||
let previousFiles = strongSelf.multiplexedNode?.files
|
||||
strongSelf.multiplexedNode?.files = files
|
||||
let wasEmpty: Bool
|
||||
if let previousFiles = previousFiles {
|
||||
wasEmpty = previousFiles.trending.isEmpty && previousFiles.saved.isEmpty
|
||||
} else {
|
||||
wasEmpty = true
|
||||
}
|
||||
let isEmpty = files.trending.isEmpty && files.saved.isEmpty
|
||||
strongSelf.emptyNode.isHidden = !isEmpty
|
||||
if wasEmpty && isEmpty {
|
||||
strongSelf.multiplexedNode?.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 60.0)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
multiplexedNode.fileSelected = { [weak self] fileReference, sourceNode, sourceRect in
|
||||
let _ = self?.controllerInteraction.sendGif(fileReference, sourceNode, sourceRect)
|
||||
}
|
||||
|
||||
multiplexedNode.fileContextMenu = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in
|
||||
@ -273,6 +259,85 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
self.updateMultiplexedNodeLayout(changedIsExpanded: false, transition: .immediate)
|
||||
|
||||
self.resetMode(synchronous: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func resetMode(synchronous: Bool) {
|
||||
let filesSignal: Signal<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*/
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -8,9 +8,13 @@ import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
|
||||
enum ChatMediaInputMetaSectionItemType {
|
||||
enum ChatMediaInputMetaSectionItemType: Equatable {
|
||||
case savedStickers
|
||||
case recentStickers
|
||||
case stickersMode
|
||||
case savedGifs
|
||||
case trendingGifs
|
||||
case gifEmoji(String)
|
||||
}
|
||||
|
||||
final class ChatMediaInputMetaSectionItem: ListViewItem {
|
||||
@ -20,7 +24,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
|
||||
let selectedItem: () -> Void
|
||||
|
||||
var selectable: Bool {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, selected: @escaping () -> Void) {
|
||||
@ -34,7 +38,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
|
||||
async {
|
||||
let node = ChatMediaInputMetaSectionItemNode()
|
||||
node.contentSize = CGSize(width: 41.0, height: 41.0)
|
||||
node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.setItem(item: self)
|
||||
node.updateTheme(theme: self.theme)
|
||||
@ -71,7 +75,10 @@ private let verticalOffset: CGFloat = 3.0 + UIScreenPixel
|
||||
|
||||
final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
private let imageNode: ASImageNode
|
||||
private let textNodeContainer: ASDisplayNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let highlightNode: ASImageNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
|
||||
var item: ChatMediaInputMetaSectionItem?
|
||||
var currentCollectionId: ItemCollectionId?
|
||||
@ -87,26 +94,57 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
|
||||
self.textNodeContainer = ASDisplayNode()
|
||||
self.textNodeContainer.isUserInteractionEnabled = false
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textNodeContainer.addSubnode(self.textNode)
|
||||
self.textNodeContainer.isUserInteractionEnabled = false
|
||||
|
||||
self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize)
|
||||
|
||||
self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
self.textNodeContainer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.highlightNode)
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.textNodeContainer)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
let imageSize = CGSize(width: 26.0, height: 26.0)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize)
|
||||
|
||||
self.textNodeContainer.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize)
|
||||
|
||||
self.buttonNode.frame = CGRect(origin: CGPoint(), size: boundingSize)
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.item?.selectedItem()
|
||||
}
|
||||
|
||||
func setItem(item: ChatMediaInputMetaSectionItem) {
|
||||
self.item = item
|
||||
switch item.type {
|
||||
case .savedStickers:
|
||||
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0)
|
||||
case .recentStickers:
|
||||
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0)
|
||||
case .savedStickers:
|
||||
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0)
|
||||
case .recentStickers:
|
||||
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,18 +155,51 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme)
|
||||
if let item = self.item {
|
||||
switch item.type {
|
||||
case .savedStickers:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme)
|
||||
case .recentStickers:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme)
|
||||
case .savedStickers:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme)
|
||||
case .recentStickers:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme)
|
||||
case .stickersMode:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelStickersModeIcon(theme)
|
||||
case .savedGifs:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme)
|
||||
case .trendingGifs:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme)
|
||||
case let .gifEmoji(emoji):
|
||||
self.imageNode.image = nil
|
||||
self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(28.0), textColor: .black)
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: floor((self.textNodeContainer.bounds.width - textSize.width) / 2.0), y: floor((self.textNodeContainer.bounds.height - textSize.height) / 2.0)), size: textSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsHighlighted() {
|
||||
if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction {
|
||||
guard let inputNodeInteraction = self.inputNodeInteraction else {
|
||||
return
|
||||
}
|
||||
if let currentCollectionId = self.currentCollectionId {
|
||||
self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId
|
||||
} else if let item = self.item {
|
||||
var isHighlighted = false
|
||||
switch item.type {
|
||||
case .savedGifs:
|
||||
if case .recent = inputNodeInteraction.highlightedGifMode {
|
||||
isHighlighted = true
|
||||
}
|
||||
case .trendingGifs:
|
||||
if case .trending = inputNodeInteraction.highlightedGifMode {
|
||||
isHighlighted = true
|
||||
}
|
||||
case let .gifEmoji(emoji):
|
||||
if case .emojiSearch(emoji) = inputNodeInteraction.highlightedGifMode {
|
||||
isHighlighted = true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.highlightNode.isHidden = !isHighlighted
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,7 +163,7 @@ func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemColle
|
||||
return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated)
|
||||
}
|
||||
|
||||
func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasGifs: Bool = true, hasUnreadTrending: Bool?, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] {
|
||||
func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasUnreadTrending: Bool?, theme: PresentationTheme, hasGifs: Bool = true) -> [ChatMediaInputPanelEntry] {
|
||||
var entries: [ChatMediaInputPanelEntry] = []
|
||||
if hasGifs {
|
||||
entries.append(.recentGifs(theme))
|
||||
@ -217,6 +217,21 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere
|
||||
return entries
|
||||
}
|
||||
|
||||
private let reactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"]
|
||||
|
||||
func chatMediaInputPanelGifModeEntries(theme: PresentationTheme) -> [ChatMediaInputPanelEntry] {
|
||||
var entries: [ChatMediaInputPanelEntry] = []
|
||||
entries.append(.stickersMode(theme))
|
||||
entries.append(.savedGifs(theme))
|
||||
entries.append(.trendingGifs(theme))
|
||||
|
||||
for reaction in reactions {
|
||||
entries.append(.gifEmotion(entries.count, theme, reaction))
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] {
|
||||
var entries: [ChatMediaInputGridEntry] = []
|
||||
|
||||
@ -331,8 +346,16 @@ enum StickerPacksCollectionUpdate {
|
||||
case navigate(ItemCollectionViewEntryIndex?, ItemCollectionId?)
|
||||
}
|
||||
|
||||
enum ChatMediaInputGifMode: Equatable {
|
||||
case recent
|
||||
case trending
|
||||
case emojiSearch(String)
|
||||
}
|
||||
|
||||
final class ChatMediaInputNodeInteraction {
|
||||
let navigateToCollectionId: (ItemCollectionId) -> Void
|
||||
let navigateBackToStickers: () -> Void
|
||||
let setGifMode: (ChatMediaInputGifMode) -> Void
|
||||
let openSettings: () -> Void
|
||||
let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void
|
||||
let openPeerSpecificSettings: () -> Void
|
||||
@ -342,11 +365,14 @@ final class ChatMediaInputNodeInteraction {
|
||||
var stickerSettings: ChatInterfaceStickerSettings?
|
||||
var highlightedStickerItemCollectionId: ItemCollectionId?
|
||||
var highlightedItemCollectionId: ItemCollectionId?
|
||||
var highlightedGifMode: ChatMediaInputGifMode = .recent
|
||||
var previewedStickerPackItem: StickerPreviewPeekItem?
|
||||
var appearanceTransition: CGFloat = 1.0
|
||||
|
||||
init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) {
|
||||
init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, navigateBackToStickers: @escaping () -> Void, setGifMode: @escaping (ChatMediaInputGifMode) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) {
|
||||
self.navigateToCollectionId = navigateToCollectionId
|
||||
self.navigateBackToStickers = navigateBackToStickers
|
||||
self.setGifMode = setGifMode
|
||||
self.openSettings = openSettings
|
||||
self.toggleSearch = toggleSearch
|
||||
self.openPeerSpecificSettings = openPeerSpecificSettings
|
||||
@ -413,6 +439,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private let listView: ListView
|
||||
private let gifListView: ListView
|
||||
private var searchContainerNode: PaneSearchContainerNode?
|
||||
private let searchContainerNodeLoadedDisposable = MetaDisposable()
|
||||
|
||||
@ -475,9 +502,12 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.listView = ListView()
|
||||
self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0)
|
||||
|
||||
self.gifListView = ListView()
|
||||
self.gifListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0)
|
||||
|
||||
var paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)?
|
||||
var fixPaneScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void)?
|
||||
var openGifContextMenuImpl: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
var openGifContextMenuImpl: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
|
||||
self.stickerPane = ChatMediaInputStickerPane(theme: theme, strings: strings, paneDidScroll: { pane, state, transition in
|
||||
paneDidScrollImpl?(pane, state, transition)
|
||||
@ -488,8 +518,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
paneDidScrollImpl?(pane, state, transition)
|
||||
}, fixPaneScroll: { pane, state in
|
||||
fixPaneScrollImpl?(pane, state)
|
||||
}, openGifContextMenu: { fileReference, sourceNode, sourceRect, gesture, isSaved in
|
||||
openGifContextMenuImpl?(fileReference, sourceNode, sourceRect, gesture, isSaved)
|
||||
}, openGifContextMenu: { file, sourceNode, sourceRect, gesture, isSaved in
|
||||
openGifContextMenuImpl?(file, sourceNode, sourceRect, gesture, isSaved)
|
||||
})
|
||||
|
||||
var getItemIsPreviewedImpl: ((StickerPackItem) -> Bool)?
|
||||
@ -546,6 +576,23 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, navigateBackToStickers: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring))
|
||||
}, setGifMode: { [weak self] mode in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.gifPane.setMode(mode: mode)
|
||||
strongSelf.inputNodeInteraction.highlightedGifMode = strongSelf.gifPane.mode
|
||||
|
||||
strongSelf.gifListView.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode {
|
||||
itemNode.updateIsHighlighted()
|
||||
}
|
||||
}
|
||||
}, openSettings: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let controller = installedStickerPacksController(context: context, mode: .modal)
|
||||
@ -563,8 +610,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self?.searchContainerNode?.deactivate()
|
||||
self?.inputNodeInteraction.toggleSearch(false, nil, "")
|
||||
})
|
||||
searchContainerNode?.openGifContextMenu = { fileReference, sourceNode, sourceRect, gesture, isSaved in
|
||||
self?.openGifContextMenu(fileReference: fileReference, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
|
||||
searchContainerNode?.openGifContextMenu = { file, sourceNode, sourceRect, gesture, isSaved in
|
||||
self?.openGifContextMenu(file: file, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
|
||||
}
|
||||
strongSelf.searchContainerNode = searchContainerNode
|
||||
if !query.isEmpty {
|
||||
@ -644,6 +691,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor
|
||||
|
||||
self.collectionListPanel.addSubnode(self.listView)
|
||||
self.collectionListPanel.addSubnode(self.gifListView)
|
||||
self.gifListView.isHidden = true
|
||||
self.collectionListContainer.addSubnode(self.collectionListPanel)
|
||||
self.collectionListContainer.addSubnode(self.collectionListSeparator)
|
||||
self.addSubnode(self.collectionListContainer)
|
||||
@ -695,7 +744,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
self.inputNodeInteraction.stickerSettings = self.controllerInteraction.stickerSettings
|
||||
|
||||
let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], []))
|
||||
let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [], []))
|
||||
|
||||
let inputNodeInteraction = self.inputNodeInteraction!
|
||||
let peerSpecificPack: Signal<(PeerSpecificPackData?, CanInstallPeerSpecificPack), NoError>
|
||||
@ -779,7 +828,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
let previousView = Atomic<ItemCollectionsView?>(value: nil)
|
||||
let transitionQueue = Queue()
|
||||
let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get())
|
||||
|> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in
|
||||
|> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in
|
||||
let (view, viewUpdate) = viewAndUpdate
|
||||
let previous = previousView.swap(view)
|
||||
var update = viewUpdate
|
||||
@ -815,6 +864,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, hasUnreadTrending: hasUnreadTrending, theme: theme)
|
||||
let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme)
|
||||
var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, strings: strings, theme: theme)
|
||||
|
||||
if view.higher == nil {
|
||||
@ -833,21 +883,19 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
}
|
||||
|
||||
let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries))
|
||||
return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty)
|
||||
let (previousPanelEntries, previousGifPaneEntries, previousGridEntries) = previousEntries.swap((panelEntries, gifPaneEntries, gridEntries))
|
||||
return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), preparedChatMediaInputPanelEntryTransition(context: context, from: previousGifPaneEntries, to: gifPaneEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty)
|
||||
}
|
||||
|
||||
self.disposable.set((transitions
|
||||
|> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, gifPaneTransition, panelFirstTime, gridTransition, gridFirstTime) in
|
||||
if let strongSelf = self {
|
||||
strongSelf.currentView = view
|
||||
strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime)
|
||||
strongSelf.enqueueGifPanelTransition(gifPaneTransition, firstTime: false)
|
||||
if !strongSelf.initializedArrangement {
|
||||
strongSelf.initializedArrangement = true
|
||||
var currentPane = strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex]
|
||||
if view.entries.isEmpty {
|
||||
//currentPane = .trending
|
||||
}
|
||||
let currentPane = strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex]
|
||||
if currentPane != strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] {
|
||||
strongSelf.setCurrentPane(currentPane, transition: .immediate)
|
||||
}
|
||||
@ -869,7 +917,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
}
|
||||
if let collectionId = topVisibleCollectionId {
|
||||
if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId {
|
||||
if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId && strongSelf.inputNodeInteraction.highlightedItemCollectionId?.namespace != ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue {
|
||||
strongSelf.setHighlightedItemCollectionId(collectionId)
|
||||
}
|
||||
}
|
||||
@ -911,8 +959,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self?.fixPaneScroll(pane: pane, state: state)
|
||||
}
|
||||
|
||||
openGifContextMenuImpl = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in
|
||||
self?.openGifContextMenu(fileReference: fileReference, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
|
||||
openGifContextMenuImpl = { [weak self] file, sourceNode, sourceRect, gesture, isSaved in
|
||||
self?.openGifContextMenu(file: file, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
|
||||
}
|
||||
}
|
||||
|
||||
@ -921,9 +969,9 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.searchContainerNodeLoadedDisposable.dispose()
|
||||
}
|
||||
|
||||
private func openGifContextMenu(fileReference: FileMediaReference, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
|
||||
private func openGifContextMenu(file: MultiplexedVideoNodeFile, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
|
||||
let canSaveGif: Bool
|
||||
if fileReference.media.fileId.namespace == Namespaces.Media.CloudFile {
|
||||
if file.file.media.fileId.namespace == Namespaces.Media.CloudFile {
|
||||
canSaveGif = true
|
||||
} else {
|
||||
canSaveGif = false
|
||||
@ -933,14 +981,14 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
if !canSaveGif {
|
||||
return false
|
||||
}
|
||||
return isGifSaved(transaction: transaction, mediaId: fileReference.media.fileId)
|
||||
return isGifSaved(transaction: transaction, mediaId: file.file.media.fileId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] isGifSaved in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [fileReference.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [])
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [])
|
||||
|
||||
let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in
|
||||
}, baseNavigationController: nil)
|
||||
@ -949,7 +997,11 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.strings.MediaPicker_Send, icon: { _ in nil }, action: { _, f in
|
||||
f(.default)
|
||||
let _ = self?.controllerInteraction.sendGif(fileReference, sourceNode, sourceRect)
|
||||
if isSaved {
|
||||
let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect)
|
||||
} else if let (collection, result) = file.contextResult {
|
||||
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect)
|
||||
}
|
||||
})))
|
||||
if isSaved || isGifSaved {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { _ in nil }, action: { _, f in
|
||||
@ -958,7 +1010,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: fileReference.media.fileId).start()
|
||||
let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.file.media.fileId).start()
|
||||
})))
|
||||
} else if canSaveGif && !isGifSaved {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Preview_SaveGif, icon: { _ in nil }, action: { _, f in
|
||||
@ -967,7 +1019,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: fileReference).start()
|
||||
let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: file.file).start()
|
||||
})))
|
||||
}
|
||||
|
||||
@ -1006,7 +1058,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
||||
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
let panes: [ASDisplayNode]
|
||||
if let searchContainerNode = strongSelf.searchContainerNode {
|
||||
@ -1071,23 +1123,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else if let file = item as? FileMediaReference {
|
||||
} else if let _ = item as? FileMediaReference {
|
||||
return nil
|
||||
/*return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.context.account, contextResult: .internalReference(queryId: 0, id: "", type: "gif", title: nil, description: nil, image: nil, file: file.media, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [
|
||||
PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.controllerInteraction.sendGif(file, node, rect)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
PeekControllerMenuItem(title: strongSelf.strings.Preview_SaveGif, color: .accent, action: { _, _ in
|
||||
if let strongSelf = self {
|
||||
let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: file).start()
|
||||
}
|
||||
return true
|
||||
})
|
||||
])))*/
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -1103,21 +1140,6 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
if let pane = pane as? ChatMediaInputGifPane {
|
||||
if let (_, _, _) = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) {
|
||||
return nil
|
||||
/*return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.context.account, contextResult: .internalReference(queryId: 0, id: "", type: "gif", title: nil, description: nil, image: nil, file: file.media, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [
|
||||
PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.controllerInteraction.sendGif(file, node, rect)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
PeekControllerMenuItem(title: strongSelf.strings.Common_Delete, color: .destructive, action: { _, _ in
|
||||
if let strongSelf = self {
|
||||
let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.media.fileId).start()
|
||||
}
|
||||
return true
|
||||
})
|
||||
])))*/
|
||||
}
|
||||
} else if pane is ChatMediaInputStickerPane || pane is ChatMediaInputTrendingPane {
|
||||
var itemNodeAndItem: (ASDisplayNode, StickerPackItem)?
|
||||
@ -1208,7 +1230,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
strongSelf.updatePreviewingItem(item: item, animated: true)
|
||||
}
|
||||
}))
|
||||
})
|
||||
self.view.addGestureRecognizer(peekRecognizer)
|
||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
self.panRecognizer = panRecognizer
|
||||
self.view.addGestureRecognizer(panRecognizer)
|
||||
@ -1283,6 +1306,31 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.inputNodeInteraction.highlightedItemCollectionId = collectionId
|
||||
}
|
||||
}
|
||||
|
||||
if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue && self.gifListView.isHidden {
|
||||
self.listView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.bounds.width, y: 0.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak self] completed in
|
||||
guard let strongSelf = self, completed else {
|
||||
return
|
||||
}
|
||||
strongSelf.listView.isHidden = true
|
||||
strongSelf.listView.layer.removeAllAnimations()
|
||||
})
|
||||
self.gifListView.layer.removeAllAnimations()
|
||||
self.gifListView.isHidden = false
|
||||
self.gifListView.layer.animatePosition(from: CGPoint(x: -self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
} else if !self.gifListView.isHidden {
|
||||
self.gifListView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -self.bounds.width, y: 0.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak self] completed in
|
||||
guard let strongSelf = self, completed else {
|
||||
return
|
||||
}
|
||||
strongSelf.gifListView.isHidden = true
|
||||
strongSelf.gifListView.layer.removeAllAnimations()
|
||||
})
|
||||
self.listView.layer.removeAllAnimations()
|
||||
self.listView.isHidden = false
|
||||
self.listView.layer.animatePosition(from: CGPoint(x: self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
|
||||
var ensuredNodeVisible = false
|
||||
var firstVisibleCollectionId: ItemCollectionId?
|
||||
self.listView.forEachItemNode { itemNode in
|
||||
@ -1374,6 +1422,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
|
||||
self.inputNodeInteraction.appearanceTransition = max(0.1, value)
|
||||
transition.updateAlpha(node: self.listView, alpha: value)
|
||||
transition.updateAlpha(node: self.gifListView, alpha: value)
|
||||
self.listView.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode {
|
||||
itemNode.updateAppearanceTransition(transition: transition)
|
||||
@ -1389,6 +1438,11 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
itemNode.updateAppearanceTransition(transition: transition)
|
||||
}
|
||||
}
|
||||
self.gifListView.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode {
|
||||
itemNode.updateAppearanceTransition(transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func simulateUpdateLayout(isVisible: Bool) {
|
||||
@ -1486,11 +1540,16 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width)
|
||||
transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0))
|
||||
|
||||
self.gifListView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width)
|
||||
transition.updatePosition(node: self.gifListView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0))
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 4.0 + leftInset, left: 0.0, bottom: 4.0 + rightInset, right: 0.0), duration: duration, curve: curve)
|
||||
|
||||
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
self.gifListView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
var visiblePanes: [(ChatMediaInputPaneType, CGFloat)] = []
|
||||
|
||||
var paneIndex = 0
|
||||
@ -1508,7 +1567,11 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
case .gifs:
|
||||
if self.gifPane.supernode == nil {
|
||||
if !displaySearch {
|
||||
self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer)
|
||||
if let searchContainerNode = self.searchContainerNode {
|
||||
self.insertSubnode(self.gifPane, belowSubnode: searchContainerNode)
|
||||
} else {
|
||||
self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer)
|
||||
}
|
||||
if self.searchContainerNode == nil {
|
||||
self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 0.0), size: CGSize(width: width, height: panelHeight))
|
||||
}
|
||||
@ -1520,22 +1583,17 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
case .stickers:
|
||||
if self.stickerPane.supernode == nil {
|
||||
self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer)
|
||||
if let searchContainerNode = self.searchContainerNode {
|
||||
self.insertSubnode(self.stickerPane, belowSubnode: searchContainerNode)
|
||||
} else {
|
||||
self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer)
|
||||
}
|
||||
self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight))
|
||||
}
|
||||
if self.stickerPane.frame != paneFrame {
|
||||
self.stickerPane.layer.removeAnimation(forKey: "position")
|
||||
transition.updateFrame(node: self.stickerPane, frame: paneFrame)
|
||||
}
|
||||
/*case .trending:
|
||||
if self.trendingPane.supernode == nil {
|
||||
self.insertSubnode(self.trendingPane, belowSubnode: self.collectionListContainer)
|
||||
self.trendingPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight))
|
||||
}
|
||||
if self.trendingPane.frame != paneFrame {
|
||||
self.trendingPane.layer.removeAnimation(forKey: "position")
|
||||
transition.updateFrame(node: self.trendingPane, frame: paneFrame)
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@ -1694,6 +1752,14 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
})
|
||||
}
|
||||
|
||||
private func enqueueGifPanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool) {
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
self.gifListView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { _ in
|
||||
})
|
||||
}
|
||||
|
||||
private func enqueueGridTransition(_ transition: ChatMediaInputGridTransition, firstTime: Bool) {
|
||||
var itemTransition: ContainedViewLayoutTransition = .immediate
|
||||
if transition.animated {
|
||||
@ -1805,6 +1871,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size))
|
||||
transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size))
|
||||
transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0))
|
||||
transition.updatePosition(node: self.gifListView, position: CGPoint(x: self.gifListView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0))
|
||||
}
|
||||
|
||||
private func fixPaneScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState) {
|
||||
@ -1825,6 +1892,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size))
|
||||
transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size))
|
||||
transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0))
|
||||
transition.updatePosition(node: self.gifListView, position: CGPoint(x: self.gifListView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0))
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
@ -1833,7 +1901,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result
|
||||
}
|
||||
|
||||
static func setupPanelIconInsets(item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> UIEdgeInsets {
|
||||
|
@ -25,72 +25,10 @@ enum ChatMediaInputPanelEntryStableId: Hashable {
|
||||
case peerSpecific
|
||||
case trending
|
||||
case settings
|
||||
|
||||
static func ==(lhs: ChatMediaInputPanelEntryStableId, rhs: ChatMediaInputPanelEntryStableId) -> Bool {
|
||||
switch lhs {
|
||||
case .recentGifs:
|
||||
if case .recentGifs = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .savedStickers:
|
||||
if case .savedStickers = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .recentPacks:
|
||||
if case .recentPacks = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .stickerPack(lhsId):
|
||||
if case let .stickerPack(rhsId) = rhs, lhsId == rhsId {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .peerSpecific:
|
||||
if case .peerSpecific = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .trending:
|
||||
if case .trending = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .settings:
|
||||
if case .settings = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hashValue: Int {
|
||||
switch self {
|
||||
case .recentGifs:
|
||||
return 0
|
||||
case .savedStickers:
|
||||
return 1
|
||||
case .recentPacks:
|
||||
return 2
|
||||
case .trending:
|
||||
return 3
|
||||
case .settings:
|
||||
return 4
|
||||
case .peerSpecific:
|
||||
return 5
|
||||
case let .stickerPack(id):
|
||||
return id.hashValue
|
||||
}
|
||||
}
|
||||
case stickersMode
|
||||
case savedGifs
|
||||
case trendingGifs
|
||||
case gifEmotion(String)
|
||||
}
|
||||
|
||||
enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
@ -102,22 +40,35 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
case peerSpecific(theme: PresentationTheme, peer: Peer)
|
||||
case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme)
|
||||
|
||||
case stickersMode(PresentationTheme)
|
||||
case savedGifs(PresentationTheme)
|
||||
case trendingGifs(PresentationTheme)
|
||||
case gifEmotion(Int, PresentationTheme, String)
|
||||
|
||||
var stableId: ChatMediaInputPanelEntryStableId {
|
||||
switch self {
|
||||
case .recentGifs:
|
||||
return .recentGifs
|
||||
case .savedStickers:
|
||||
return .savedStickers
|
||||
case .recentPacks:
|
||||
return .recentPacks
|
||||
case .trending:
|
||||
return .trending
|
||||
case .settings:
|
||||
return .settings
|
||||
case .peerSpecific:
|
||||
return .peerSpecific
|
||||
case let .stickerPack(_, info, _, _):
|
||||
return .stickerPack(info.id.id)
|
||||
case .recentGifs:
|
||||
return .recentGifs
|
||||
case .savedStickers:
|
||||
return .savedStickers
|
||||
case .recentPacks:
|
||||
return .recentPacks
|
||||
case .trending:
|
||||
return .trending
|
||||
case .settings:
|
||||
return .settings
|
||||
case .peerSpecific:
|
||||
return .peerSpecific
|
||||
case let .stickerPack(_, info, _, _):
|
||||
return .stickerPack(info.id.id)
|
||||
case .stickersMode:
|
||||
return .stickersMode
|
||||
case .savedGifs:
|
||||
return .savedGifs
|
||||
case .trendingGifs:
|
||||
return .trendingGifs
|
||||
case let .gifEmotion(_, _, emoji):
|
||||
return .gifEmotion(emoji)
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,6 +116,30 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .stickersMode(lhsTheme):
|
||||
if case let .stickersMode(rhsTheme) = rhs, lhsTheme === rhsTheme {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .savedGifs(lhsTheme):
|
||||
if case let .savedGifs(rhsTheme) = rhs, lhsTheme === rhsTheme {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .trendingGifs(lhsTheme):
|
||||
if case let .trendingGifs(rhsTheme) = rhs, lhsTheme === rhsTheme {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji):
|
||||
if case let .gifEmotion(rhsIndex, rhsTheme, rhsEmoji) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsEmoji == rhsEmoji {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,6 +197,8 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return lhsIndex <= rhsIndex
|
||||
}
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .trending(elevated, _):
|
||||
if elevated {
|
||||
@ -238,8 +215,37 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case .settings:
|
||||
case .stickersMode:
|
||||
return false
|
||||
case .savedGifs:
|
||||
switch rhs {
|
||||
case .savedGifs:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .trendingGifs:
|
||||
switch rhs {
|
||||
case .stickersMode, .savedGifs, .trendingGifs:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .gifEmotion(lhsIndex, _, _):
|
||||
switch rhs {
|
||||
case .stickersMode, .savedGifs, .trendingGifs:
|
||||
return false
|
||||
case let .gifEmotion(rhsIndex, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .settings:
|
||||
if case .settings = rhs {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,6 +284,22 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
return ChatMediaInputStickerPackItem(account: context.account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, index: index, theme: theme, selected: {
|
||||
inputNodeInteraction.navigateToCollectionId(info.id)
|
||||
})
|
||||
case let .stickersMode(theme):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .stickersMode, theme: theme, selected: {
|
||||
inputNodeInteraction.navigateBackToStickers()
|
||||
})
|
||||
case let .savedGifs(theme):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedGifs, theme: theme, selected: {
|
||||
inputNodeInteraction.setGifMode(.recent)
|
||||
})
|
||||
case let .trendingGifs(theme):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .trendingGifs, theme: theme, selected: {
|
||||
inputNodeInteraction.setGifMode(.trending)
|
||||
})
|
||||
case let .gifEmotion(_, theme, emoji):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji), theme: theme, selected: {
|
||||
inputNodeInteraction.setGifMode(.emojiSearch(emoji))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1089,8 +1089,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
if wideLayout {
|
||||
if let size = file.size {
|
||||
let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))"
|
||||
if file.isAnimated && (!automaticDownload || !automaticPlayback) {
|
||||
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: "\(gifTitle) " + sizeString, size: nil, muted: false, active: false)
|
||||
if file.isAnimated {
|
||||
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: "\(gifTitle)", size: nil, muted: false, active: false)
|
||||
}
|
||||
else if let duration = file.duration, !message.flags.contains(.Unsent) {
|
||||
let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition)
|
||||
|
@ -195,7 +195,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in
|
||||
self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame)
|
||||
}, openMessageContextActions: { _, _, _, _ in
|
||||
}, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in
|
||||
}, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in
|
||||
self?.openUrl(url)
|
||||
}, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in
|
||||
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
|
||||
|
@ -71,7 +71,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
|
||||
|
||||
self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
return nil
|
||||
}, chatControllerNode: {
|
||||
@ -188,6 +188,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, navigateBackToStickers: {
|
||||
}, setGifMode: { _ in
|
||||
}, openSettings: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
// let controller = installedStickerPacksController(context: context, mode: .modal)
|
||||
@ -363,7 +365,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
}
|
||||
}
|
||||
|
||||
let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasGifs: false, hasUnreadTrending: hasUnreadTrending, theme: theme)
|
||||
let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasUnreadTrending: hasUnreadTrending, theme: theme, hasGifs: false)
|
||||
var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, strings: strings, theme: theme)
|
||||
|
||||
if view.higher == nil {
|
||||
|
@ -272,6 +272,10 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
|
||||
let inputNodeInteraction = ChatMediaInputNodeInteraction(
|
||||
navigateToCollectionId: { _ in
|
||||
},
|
||||
navigateBackToStickers: {
|
||||
},
|
||||
setGifMode: { _ in
|
||||
},
|
||||
openSettings: {
|
||||
},
|
||||
toggleSearch: { _, _, _ in
|
||||
|
@ -11,9 +11,7 @@ import AccountContext
|
||||
import WebSearchUI
|
||||
import AppBundle
|
||||
|
||||
func paneGifSearchForQuery(account: Account, query: String, offset: String?, updateActivity: ((Bool) -> Void)?) -> Signal<([FileMediaReference], String?)?, NoError> {
|
||||
let delayRequest = true
|
||||
|
||||
func paneGifSearchForQuery(account: Account, query: String, offset: String?, incompleteResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> {
|
||||
let contextBot = account.postbox.transaction { transaction -> String in
|
||||
let configuration = currentSearchBotsConfiguration(transaction: transaction)
|
||||
return configuration.gifBotUsername ?? "gif"
|
||||
@ -24,35 +22,23 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, upd
|
||||
|> mapToSignal { peerId -> Signal<Peer?, NoError> in
|
||||
if let peerId = peerId {
|
||||
return account.postbox.loadedPeerWithId(peerId)
|
||||
|> map { peer -> Peer? in
|
||||
return peer
|
||||
}
|
||||
|> take(1)
|
||||
|> map { peer -> Peer? in
|
||||
return peer
|
||||
}
|
||||
|> take(1)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in
|
||||
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
|
||||
let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, offset: offset ?? "", limit: 50)
|
||||
let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, limit: 1)
|
||||
|> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
return { _ in
|
||||
return .contextRequestResult(user, results)
|
||||
}
|
||||
}
|
||||
|
||||
let botResult: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ previousResult in
|
||||
var passthroughPreviousResult: ChatContextResultCollection?
|
||||
if let previousResult = previousResult {
|
||||
if case let .contextRequestResult(previousUser, previousResults) = previousResult {
|
||||
if previousUser?.id == user.id {
|
||||
passthroughPreviousResult = previousResults
|
||||
}
|
||||
}
|
||||
}
|
||||
return .contextRequestResult(nil, passthroughPreviousResult)
|
||||
})
|
||||
|
||||
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>
|
||||
if delayRequest {
|
||||
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
|
||||
@ -60,15 +46,16 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, upd
|
||||
maybeDelayedContextResults = results
|
||||
}
|
||||
|
||||
return botResult |> then(maybeDelayedContextResults)
|
||||
return maybeDelayedContextResults
|
||||
} else {
|
||||
return .single({ _ in return nil })
|
||||
}
|
||||
}
|
||||
return contextBot
|
||||
|> mapToSignal { result -> Signal<([FileMediaReference], String?)?, NoError> in
|
||||
if let r = result(nil), case let .contextRequestResult(_, collection) = r, let results = collection?.results {
|
||||
var references: [FileMediaReference] = []
|
||||
|> mapToSignal { result -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> in
|
||||
if let r = result(nil), case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
|
||||
let results = collection.results
|
||||
var references: [MultiplexedVideoNodeFile] = []
|
||||
for result in results {
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
@ -106,15 +93,17 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, upd
|
||||
}
|
||||
}
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
|
||||
references.append(FileMediaReference.standalone(media: file))
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let file = internalReference.file {
|
||||
references.append(FileMediaReference.standalone(media: file))
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return .single((references, collection?.nextOffset))
|
||||
return .single((references, collection.nextOffset))
|
||||
} else if incompleteResults {
|
||||
return .single(nil)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
@ -145,7 +134,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let trendingPromise: Promise<[FileMediaReference]?>
|
||||
private let trendingPromise: Promise<[MultiplexedVideoNodeFile]?>
|
||||
private let searchDisposable = MetaDisposable()
|
||||
|
||||
private let _ready = Promise<Void>()
|
||||
@ -156,11 +145,11 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
var deactivateSearchBar: (() -> Void)?
|
||||
var updateActivity: ((Bool) -> Void)?
|
||||
var requestUpdateQuery: ((String) -> Void)?
|
||||
var openGifContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
|
||||
private var hasInitialText = false
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[FileMediaReference]?>) {
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[MultiplexedVideoNodeFile]?>) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
@ -198,13 +187,13 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
self.hasInitialText = true
|
||||
self.isLoadingNextResults = true
|
||||
|
||||
let signal: Signal<([FileMediaReference], String?)?, NoError>
|
||||
let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError>
|
||||
if !text.isEmpty {
|
||||
signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: "", updateActivity: self.updateActivity)
|
||||
self.updateActivity?(true)
|
||||
} else {
|
||||
signal = self.trendingPromise.get()
|
||||
|> map { items -> ([FileMediaReference], String?)? in
|
||||
|> map { items -> ([MultiplexedVideoNodeFile], String?)? in
|
||||
if let items = items {
|
||||
return (items, nil)
|
||||
} else {
|
||||
@ -226,7 +215,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
} else {
|
||||
strongSelf.nextOffset = nil
|
||||
}
|
||||
strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: result)
|
||||
strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: result, isSearch: true), synchronous: true, resetScrollingToOffset: nil)
|
||||
strongSelf.updateActivity?(false)
|
||||
strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty
|
||||
}))
|
||||
@ -241,7 +230,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
}
|
||||
self.isLoadingNextResults = true
|
||||
|
||||
let signal: Signal<([FileMediaReference], String?)?, NoError>
|
||||
let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError>
|
||||
signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity)
|
||||
|
||||
self.searchDisposable.set((signal
|
||||
@ -251,12 +240,12 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
}
|
||||
|
||||
var files = strongSelf.multiplexedNode?.files.trending ?? []
|
||||
var currentIds = Set(files.map { $0.media.fileId })
|
||||
var currentIds = Set(files.map { $0.file.media.fileId })
|
||||
for item in result {
|
||||
if currentIds.contains(item.media.fileId) {
|
||||
if currentIds.contains(item.file.media.fileId) {
|
||||
continue
|
||||
}
|
||||
currentIds.insert(item.media.fileId)
|
||||
currentIds.insert(item.file.media.fileId)
|
||||
files.append(item)
|
||||
}
|
||||
|
||||
@ -266,7 +255,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
} else {
|
||||
strongSelf.nextOffset = nil
|
||||
}
|
||||
strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: files)
|
||||
strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: files, isSearch: true), synchronous: true, resetScrollingToOffset: nil)
|
||||
strongSelf.notFoundNode.isHidden = text.isEmpty || !files.isEmpty
|
||||
}))
|
||||
}
|
||||
@ -326,8 +315,12 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
|
||||
self.addSubnode(multiplexedNode)
|
||||
|
||||
multiplexedNode.fileSelected = { [weak self] fileReference, sourceNode, sourceRect in
|
||||
let _ = self?.controllerInteraction.sendGif(fileReference, sourceNode, sourceRect)
|
||||
multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in
|
||||
if let (collection, result) = file.contextResult {
|
||||
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect)
|
||||
} else {
|
||||
let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect)
|
||||
}
|
||||
}
|
||||
|
||||
multiplexedNode.fileContextMenu = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in
|
||||
|
@ -32,112 +32,39 @@ private final class VisibleVideoItem {
|
||||
case trending(MediaId)
|
||||
}
|
||||
let id: Id
|
||||
let fileReference: FileMediaReference
|
||||
let file: MultiplexedVideoNodeFile
|
||||
let frame: CGRect
|
||||
|
||||
init(fileReference: FileMediaReference, frame: CGRect, isTrending: Bool) {
|
||||
self.fileReference = fileReference
|
||||
init(file: MultiplexedVideoNodeFile, frame: CGRect, isTrending: Bool) {
|
||||
self.file = file
|
||||
self.frame = frame
|
||||
if isTrending {
|
||||
self.id = .trending(fileReference.media.fileId)
|
||||
self.id = .trending(file.file.media.fileId)
|
||||
} else {
|
||||
self.id = .saved(fileReference.media.fileId)
|
||||
self.id = .saved(file.file.media.fileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class MultiplexedVideoNodeFile {
|
||||
let file: FileMediaReference
|
||||
let contextResult: (ChatContextResultCollection, ChatContextResult)?
|
||||
|
||||
init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) {
|
||||
self.file = file
|
||||
self.contextResult = contextResult
|
||||
}
|
||||
}
|
||||
|
||||
final class MultiplexedVideoNodeFiles {
|
||||
let saved: [FileMediaReference]
|
||||
let trending: [FileMediaReference]
|
||||
let saved: [MultiplexedVideoNodeFile]
|
||||
let trending: [MultiplexedVideoNodeFile]
|
||||
let isSearch: Bool
|
||||
|
||||
init(saved: [FileMediaReference], trending: [FileMediaReference]) {
|
||||
init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool) {
|
||||
self.saved = saved
|
||||
self.trending = trending
|
||||
}
|
||||
}
|
||||
|
||||
private final class TrendingHeaderNode: ASDisplayNode {
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let reactions: [String]
|
||||
private let reactionNodes: [ImmediateTextNode]
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
var reactionSelected: ((String) -> Void)?
|
||||
|
||||
override init() {
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.reactions = [
|
||||
"👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"
|
||||
]
|
||||
self.scrollNode = ASScrollNode()
|
||||
let scrollNode = self.scrollNode
|
||||
self.reactionNodes = reactions.map { reaction -> ImmediateTextNode in
|
||||
let textNode = ImmediateTextNode()
|
||||
textNode.attributedText = NSAttributedString(string: reaction, font: Font.regular(30.0), textColor: .black)
|
||||
scrollNode.addSubnode(textNode)
|
||||
return textNode
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.canCancelContentTouches = true
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
self.reactionNodes[i].view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
}
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let location = recognizer.location(in: self.scrollNode.view)
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
if self.reactionNodes[i].frame.contains(location) {
|
||||
let reaction = self.reactions[i]
|
||||
self.reactionSelected?(reaction)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, strings: PresentationStrings, width: CGFloat, sideInset: CGFloat) -> CGFloat {
|
||||
let height: CGFloat = 72.0
|
||||
let leftInset: CGFloat = 10.0
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_TrendingSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset * 2.0 - sideInset * 2.0, height: 100.0))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: titleSize)
|
||||
|
||||
let reactionSizes = self.reactionNodes.map { reactionNode -> CGSize in
|
||||
return reactionNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
}
|
||||
|
||||
let reactionSpacing: CGFloat = 8.0
|
||||
var reactionsOffset: CGFloat = leftInset - 2.0
|
||||
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
if i != 0 {
|
||||
reactionsOffset += reactionSpacing
|
||||
}
|
||||
reactionNodes[i].frame = CGRect(origin: CGPoint(x: reactionsOffset, y: 0.0), size: reactionSizes[i])
|
||||
reactionsOffset += reactionSizes[i].width
|
||||
}
|
||||
reactionsOffset += leftInset - 2.0
|
||||
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 28.0), size: CGSize(width: width, height: 44.0))
|
||||
self.scrollNode.view.contentSize = CGSize(width: reactionsOffset, height: 44.0)
|
||||
|
||||
return height
|
||||
self.isSearch = isSearch
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,13 +95,19 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: []) {
|
||||
didSet {
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: true)
|
||||
print("MultiplexedVideoNode files updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false)
|
||||
|
||||
func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) {
|
||||
self.files = files
|
||||
|
||||
self.ignoreDidScroll = true
|
||||
if let resetScrollingToOffset = resetScrollingToOffset {
|
||||
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y :resetScrollingToOffset)
|
||||
}
|
||||
self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: synchronous)
|
||||
self.ignoreDidScroll = false
|
||||
}
|
||||
|
||||
private var displayItems: [VisibleVideoItem] = []
|
||||
private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:]
|
||||
private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:]
|
||||
@ -185,7 +118,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
|
||||
|
||||
private let savedTitleNode: ImmediateTextNode
|
||||
private let trendingHeaderNode: TrendingHeaderNode
|
||||
private let trendingTitleNode: ImmediateTextNode
|
||||
|
||||
private var displayLink: CADisplayLink!
|
||||
private var timeOffset = 0.0
|
||||
@ -193,8 +126,8 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
private let timebase: CMTimebase
|
||||
|
||||
var fileSelected: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)?
|
||||
var fileContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)?
|
||||
var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
var enableVideoNodes = false
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
@ -215,21 +148,18 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.savedTitleNode = ImmediateTextNode()
|
||||
self.savedTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_SavedSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
|
||||
self.trendingHeaderNode = TrendingHeaderNode()
|
||||
self.trendingTitleNode = ImmediateTextNode()
|
||||
self.trendingTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_TrendingSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
|
||||
super.init()
|
||||
|
||||
self.trendingHeaderNode.reactionSelected = { [weak self] reaction in
|
||||
self?.reactionSelected?(reaction)
|
||||
}
|
||||
|
||||
self.isOpaque = true
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.alwaysBounceVertical = true
|
||||
|
||||
self.scrollNode.addSubnode(self.savedTitleNode)
|
||||
self.scrollNode.addSubnode(self.trendingHeaderNode)
|
||||
self.scrollNode.addSubnode(self.trendingTitleNode)
|
||||
|
||||
self.addSubnode(self.trackingNode)
|
||||
self.addSubnode(self.contextContainerNode)
|
||||
@ -300,7 +230,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
self.contextContainerNode.customActivationProgress = { [weak self] progress, update in
|
||||
guard let strongSelf = self, let gestureLocation = gestureLocation else {
|
||||
guard let _ = self, let _ = gestureLocation else {
|
||||
return
|
||||
}
|
||||
/*let minScale: CGFloat = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width
|
||||
@ -356,9 +286,13 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private var ignoreDidScroll: Bool = false
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateImmediatelyVisibleItems()
|
||||
self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height)
|
||||
if !self.ignoreDidScroll {
|
||||
self.updateImmediatelyVisibleItems()
|
||||
self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
@ -407,15 +341,12 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
thumbnailLayer.frame = item.frame
|
||||
}
|
||||
} else {
|
||||
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.fileReference, synchronousLoad: synchronous)
|
||||
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.file.file, synchronousLoad: synchronous)
|
||||
thumbnailLayer.frame = item.frame
|
||||
self.scrollNode.layer.addSublayer(thumbnailLayer)
|
||||
self.visibleThumbnailLayers[item.id] = thumbnailLayer
|
||||
}
|
||||
|
||||
let progressSize = CGSize(width: 24.0, height: 24.0)
|
||||
let progressFrame = CGRect(origin: CGPoint(x: item.frame.midX - progressSize.width / 2.0, y: item.frame.midY - progressSize.height / 2.0), size: progressSize)
|
||||
|
||||
if item.frame.maxY < minVisibleY {
|
||||
continue
|
||||
}
|
||||
@ -434,7 +365,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
layerHolder.layer.frame = item.frame
|
||||
self.scrollNode.layer.addSublayer(layerHolder.layer)
|
||||
let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.fileReference, layerHolder: layerHolder)
|
||||
let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.file.file, layerHolder: layerHolder)
|
||||
self.visibleLayers[item.id] = (manager, layerHolder)
|
||||
self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
@ -490,11 +421,9 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
if !drawableSize.width.isZero {
|
||||
var displayItems: [VisibleVideoItem] = []
|
||||
|
||||
let idealHeight = self.idealHeight
|
||||
|
||||
var verticalOffset: CGFloat = self.topInset
|
||||
|
||||
func commitFilesSpans(files: [FileMediaReference], isTrending: Bool) {
|
||||
func commitFilesSpans(files: [MultiplexedVideoNodeFile], isTrending: Bool) {
|
||||
var rowsCount = 0
|
||||
var firstRowMax = 0;
|
||||
|
||||
@ -512,7 +441,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
for a in 0 ..< itemsCount {
|
||||
var size: CGSize
|
||||
if let dimensions = files[a].media.dimensions {
|
||||
if let dimensions = files[a].file.media.dimensions {
|
||||
size = dimensions.cgSize
|
||||
} else {
|
||||
size = CGSize(width: 100.0, height: 100.0)
|
||||
@ -588,7 +517,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
if itemsToRow[index] != nil && currentRowHorizontalOffset + itemSize.width >= drawableSize.width - 10.0 {
|
||||
itemSize.width = max(itemSize.width, drawableSize.width - currentRowHorizontalOffset)
|
||||
}
|
||||
displayItems.append(VisibleVideoItem(fileReference: files[index], frame: CGRect(origin: CGPoint(x: currentRowHorizontalOffset, y: verticalOffset), size: itemSize), isTrending: isTrending))
|
||||
displayItems.append(VisibleVideoItem(file: files[index], frame: CGRect(origin: CGPoint(x: currentRowHorizontalOffset, y: verticalOffset), size: itemSize), isTrending: isTrending))
|
||||
currentRowHorizontalOffset += itemSize.width + 1.0
|
||||
|
||||
if itemsToRow[index] != nil {
|
||||
@ -598,104 +527,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func commitFiles(files: [FileMediaReference], isTrending: Bool) {
|
||||
var weights: [Int] = []
|
||||
var totalItemSize: CGFloat = 0.0
|
||||
for item in files {
|
||||
let aspectRatio: CGFloat
|
||||
if let dimensions = item.media.dimensions {
|
||||
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
|
||||
} else {
|
||||
aspectRatio = 1.0
|
||||
}
|
||||
weights.append(Int(aspectRatio * 100))
|
||||
totalItemSize += aspectRatio * idealHeight
|
||||
}
|
||||
|
||||
let numberOfRows = max(Int(round(totalItemSize / drawableSize.width)), 1)
|
||||
|
||||
let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows)
|
||||
|
||||
var i = 0
|
||||
var offset = CGPoint(x: 0.0, y: verticalOffset)
|
||||
var previousItemSize: CGFloat = 0.0
|
||||
let maxWidth = drawableSize.width
|
||||
|
||||
let minimumInteritemSpacing: CGFloat = 1.0
|
||||
let minimumLineSpacing: CGFloat = 1.0
|
||||
|
||||
let viewportWidth: CGFloat = drawableSize.width
|
||||
|
||||
let preferredRowSize = idealHeight
|
||||
|
||||
var rowIndex = -1
|
||||
for row in partition {
|
||||
rowIndex += 1
|
||||
|
||||
var summedRatios: CGFloat = 0.0
|
||||
|
||||
var j = i
|
||||
var n = i + row.count
|
||||
|
||||
while j < n {
|
||||
let aspectRatio: CGFloat
|
||||
if let dimensions = files[j].media.dimensions {
|
||||
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
|
||||
} else {
|
||||
aspectRatio = 1.0
|
||||
}
|
||||
|
||||
summedRatios += aspectRatio
|
||||
|
||||
j += 1
|
||||
}
|
||||
|
||||
var rowSize = drawableSize.width - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
|
||||
if rowIndex == partition.count - 1 {
|
||||
if row.count < 2 {
|
||||
rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
} else if row.count < 3 {
|
||||
rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
j = i
|
||||
n = i + row.count
|
||||
|
||||
while j < n {
|
||||
let aspectRatio: CGFloat
|
||||
if let dimensions = files[j].media.dimensions {
|
||||
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
|
||||
} else {
|
||||
aspectRatio = 1.0
|
||||
}
|
||||
let preferredAspectRatio = aspectRatio
|
||||
|
||||
let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize)
|
||||
|
||||
var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height)
|
||||
if frame.origin.x + frame.size.width >= maxWidth - 2.0 {
|
||||
frame.size.width = max(1.0, maxWidth - frame.origin.x)
|
||||
}
|
||||
|
||||
displayItems.append(VisibleVideoItem(fileReference: files[j], frame: frame, isTrending: isTrending))
|
||||
|
||||
offset.x += actualSize.width + minimumInteritemSpacing
|
||||
previousItemSize = actualSize.height
|
||||
verticalOffset = frame.maxY
|
||||
|
||||
j += 1
|
||||
}
|
||||
|
||||
if row.count > 0 {
|
||||
offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing)
|
||||
}
|
||||
|
||||
i += row.count
|
||||
}
|
||||
}
|
||||
|
||||
var hasContent = false
|
||||
if !self.files.saved.isEmpty {
|
||||
self.savedTitleNode.isHidden = false
|
||||
let leftInset: CGFloat = 10.0
|
||||
@ -703,19 +535,26 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.savedTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: savedTitleSize)
|
||||
verticalOffset += savedTitleSize.height + 5.0
|
||||
commitFilesSpans(files: self.files.saved, isTrending: false)
|
||||
//commitFiles(files: self.files.saved, isTrending: false)
|
||||
hasContent = true
|
||||
} else {
|
||||
self.savedTitleNode.isHidden = true
|
||||
}
|
||||
if !self.files.trending.isEmpty {
|
||||
self.trendingHeaderNode.isHidden = false
|
||||
let trendingHeight = self.trendingHeaderNode.update(theme: self.theme, strings: self.strings, width: drawableSize.width, sideInset: 0.0)
|
||||
self.trendingHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: drawableSize.width, height: trendingHeight))
|
||||
verticalOffset += trendingHeight
|
||||
if self.files.isSearch {
|
||||
self.trendingTitleNode.isHidden = true
|
||||
} else {
|
||||
self.trendingTitleNode.isHidden = false
|
||||
if hasContent {
|
||||
verticalOffset += 15.0
|
||||
}
|
||||
let leftInset: CGFloat = 10.0
|
||||
let trendingTitleSize = self.trendingTitleNode.updateLayout(CGSize(width: drawableSize.width - leftInset * 2.0, height: 100.0))
|
||||
self.trendingTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: trendingTitleSize)
|
||||
verticalOffset += trendingTitleSize.height + 5.0
|
||||
}
|
||||
commitFilesSpans(files: self.files.trending, isTrending: true)
|
||||
//commitFiles(files: self.files.trending, isTrending: true)
|
||||
} else {
|
||||
self.trendingHeaderNode.isHidden = true
|
||||
self.trendingTitleNode.isHidden = true
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: drawableSize.width, height: verticalOffset + self.bottomInset)
|
||||
@ -748,19 +587,19 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
func frameForItem(_ id: MediaId) -> CGRect? {
|
||||
for item in self.displayItems {
|
||||
if item.fileReference.media.fileId == id {
|
||||
if item.file.file.media.fileId == id {
|
||||
return item.frame
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileAt(point: CGPoint) -> (FileMediaReference, CGRect, Bool)? {
|
||||
func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? {
|
||||
let offsetPoint = point.offsetBy(dx: 0.0, dy: self.scrollNode.bounds.minY)
|
||||
return self.offsetFileAt(point: offsetPoint)
|
||||
}
|
||||
|
||||
private func offsetFileAt(point: CGPoint) -> (FileMediaReference, CGRect, Bool)? {
|
||||
private func offsetFileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? {
|
||||
for item in self.displayItems {
|
||||
if item.frame.contains(point) {
|
||||
let isSaved: Bool
|
||||
@ -770,7 +609,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
case .trending:
|
||||
isSaved = false
|
||||
}
|
||||
return (item.fileReference, item.frame, isSaved)
|
||||
return (item.file, item.frame, isSaved)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -74,6 +74,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
return false
|
||||
}, sendGif: { _, _, _ in
|
||||
return false
|
||||
}, sendBotContextResultAsGif: { _, _, _, _ in
|
||||
return false
|
||||
}, requestMessageActionCallback: { _, _, _ in
|
||||
}, requestMessageActionUrlAuth: { _, _, _ in
|
||||
}, activateSwitchInline: { _, _ in
|
||||
|
@ -39,13 +39,13 @@ final class PaneSearchContainerNode: ASDisplayNode {
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
var openGifContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
|
||||
var ready: Signal<Void, NoError> {
|
||||
return self.contentNode.ready
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<[FileMediaReference]?>, cancel: @escaping () -> Void) {
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<[MultiplexedVideoNodeFile]?>, cancel: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.controllerInteraction = controllerInteraction
|
||||
|
@ -1479,6 +1479,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
return false
|
||||
}, sendGif: { _, _, _ in
|
||||
return false
|
||||
}, sendBotContextResultAsGif: { _, _, _, _ in
|
||||
return false
|
||||
}, requestMessageActionCallback: { _, _, _ in
|
||||
}, requestMessageActionUrlAuth: { _, _, _ in
|
||||
}, activateSwitchInline: { _, _ in
|
||||
|
@ -325,6 +325,8 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
||||
return false
|
||||
}, sendGif: { _, _, _ in
|
||||
return false
|
||||
}, sendBotContextResultAsGif: { _, _, _, _ in
|
||||
return false
|
||||
}, requestMessageActionCallback: { _, _, _ in
|
||||
}, requestMessageActionUrlAuth: { _, _, _ in
|
||||
}, activateSwitchInline: { _, _ in
|
||||
|
@ -1106,7 +1106,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
tapMessage?(message)
|
||||
}, clickThroughMessage: {
|
||||
clickThroughMessage?()
|
||||
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in
|
||||
return false
|
||||
}, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
return nil
|
||||
}, chatControllerNode: {
|
||||
|
@ -109,6 +109,7 @@ final class SoftwareVideoLayerFrameManager {
|
||||
while index < self.frames.count {
|
||||
if baseTimestamp + self.frames[index].position.seconds + self.frames[index].duration.seconds <= timestamp {
|
||||
latestFrameIndex = index
|
||||
//print("latestFrameIndex = \(index)")
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
@ -139,7 +140,7 @@ final class SoftwareVideoLayerFrameManager {
|
||||
private var polling = false
|
||||
|
||||
private func poll() {
|
||||
if self.frames.count < 3 && !self.polling {
|
||||
if self.frames.count < 2 && !self.polling {
|
||||
self.polling = true
|
||||
let minPts = self.minPts
|
||||
let maxPts = self.maxPts
|
||||
@ -179,7 +180,11 @@ final class SoftwareVideoLayerFrameManager {
|
||||
}
|
||||
if let frame = frameAndLoop?.0 {
|
||||
if strongSelf.minPts == nil || CMTimeCompare(strongSelf.minPts!, frame.position) < 0 {
|
||||
strongSelf.minPts = frame.position
|
||||
var position = CMTimeAdd(frame.position, frame.duration)
|
||||
for _ in 0 ..< 1 {
|
||||
position = CMTimeAdd(position, frame.duration)
|
||||
}
|
||||
strongSelf.minPts = position
|
||||
}
|
||||
strongSelf.frames.append(frame)
|
||||
strongSelf.frames.sort(by: { lhs, rhs in
|
||||
@ -190,7 +195,7 @@ final class SoftwareVideoLayerFrameManager {
|
||||
}
|
||||
})
|
||||
//print("add frame at \(CMTimeGetSeconds(frame.position))")
|
||||
let positions = strongSelf.frames.map { CMTimeGetSeconds($0.position) }
|
||||
//let positions = strongSelf.frames.map { CMTimeGetSeconds($0.position) }
|
||||
//print("frames: \(positions)")
|
||||
} else {
|
||||
//print("not adding frames")
|
||||
|
@ -10,8 +10,8 @@ import LegacyComponents
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
|
||||
public func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, limit: Int = 60) -> Signal<ChatContextResultCollection?, NoError> {
|
||||
return requestChatContextResults(account: account, botId: botId, peerId: peerId, query: query, offset: offset)
|
||||
public func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, limit: Int = 60) -> Signal<ChatContextResultCollection?, NoError> {
|
||||
return requestChatContextResults(account: account, botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults)
|
||||
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user