GIF-related fixes part 2

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

View File

@ -4,6 +4,13 @@
NS_ASSUME_NONNULL_BEGIN
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

View File

@ -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;

View File

@ -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);
}

View File

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

View File

@ -21,10 +21,15 @@ 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) {
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
}
}
if self.delayedFrames.count >= 1 {

View File

@ -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)
}
}

View File

@ -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 {
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
}
return (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
result = (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
} else {
return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
}
} else {
return (nil, 0.0, 1.0, false)
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) {

View File

@ -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)
}
}
}
}
}
}

View File

@ -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()

View File

@ -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

View File

@ -13,12 +13,21 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
]
}
// To avoid upgrading the database, **new** tags can be added here
// Uninitialized peers will fill the info using messageHoles
var upgradedMessageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]] = [:]
for peerNamespace in peerIdNamespacesWithInitialCloudMessageHoles {
upgradedMessageHoles[peerNamespace] = [
Namespaces.Message.Cloud: Set(MessageTags.gif)
]
}
var globalMessageIdsPeerIdNamespaces = Set<GlobalMessageIdsNamespace>()
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

View File

@ -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

View File

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

View File

@ -214,6 +214,28 @@ public struct PresentationResourcesChat {
})
}
public static func chatInputMediaPanelStickersModeIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatInputMediaPanelStickersModeIcon.rawValue, { theme in
return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/StickersMode"), color: theme.chat.inputMediaPanel.panelIconColor) {
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
}
})
})
}
public static func chatInputMediaPanelTrendingGifsIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatInputMediaPanelTrendingGifsIcon.rawValue, { theme in
return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/TrendingGifs"), color: theme.chat.inputMediaPanel.panelIconColor) {
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
}
})
})
}
public static func chatInputMediaPanelRecentStickersIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatInputMediaPanelRecentStickersIconImage.rawValue, { theme in
return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in

View File

@ -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

View File

@ -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: 1.1 KiB

View File

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

View File

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

View File

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

View File

@ -766,6 +766,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)])
}
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
})
}
})

View File

@ -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: {

View File

@ -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*/
}
}))
}
}

View File

@ -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,17 +94,46 @@ 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) {
@ -107,6 +143,8 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0)
case .recentStickers:
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0)
default:
break
}
}
@ -121,14 +159,47 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme)
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
}
}

View File

@ -163,7 +163,7 @@ func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemColle
return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated)
}
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 {
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 {
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 {

View File

@ -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,6 +40,11 @@ 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:
@ -118,6 +61,14 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
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))
})
}
}
}

View File

@ -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)

View File

@ -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() {

View File

@ -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 {

View File

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

View File

@ -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"
@ -34,25 +32,13 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, upd
}
|> 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

View File

@ -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,10 +286,14 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
}
private var ignoreDidScroll: Bool = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreDidScroll {
self.updateImmediatelyVisibleItems()
self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height)
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.didEndScrolling?()
@ -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
commitFilesSpans(files: self.files.trending, isTrending: true)
//commitFiles(files: self.files.trending, isTrending: true)
if self.files.isSearch {
self.trendingTitleNode.isHidden = true
} else {
self.trendingHeaderNode.isHidden = true
self.trendingTitleNode.isHidden = false
if hasContent {
verticalOffset += 15.0
}
let leftInset: CGFloat = 10.0
let trendingTitleSize = self.trendingTitleNode.updateLayout(CGSize(width: drawableSize.width - leftInset * 2.0, height: 100.0))
self.trendingTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: trendingTitleSize)
verticalOffset += trendingTitleSize.height + 5.0
}
commitFilesSpans(files: self.files.trending, isTrending: true)
} else {
self.trendingTitleNode.isHidden = true
}
let contentSize = CGSize(width: drawableSize.width, height: verticalOffset + self.bottomInset)
@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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: {

View File

@ -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")

View File

@ -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)
}