Emoji input improvements

This commit is contained in:
Ali 2022-07-21 03:30:22 +02:00
parent fdea4fc967
commit 806536a4cd
14 changed files with 1604 additions and 320 deletions

View File

@ -107,6 +107,7 @@
"PUSH_MESSAGE_THEME" = "%1$@|changed chat theme to %2$@";
"PUSH_MESSAGE_NOTHEME" = "%1$@|disabled chat theme";
"PUSH_MESSAGE_RECURRING_PAY" = "%1$@|You were charged %2$@";
"CHAT_MESSAGE_RECURRING_PAY" = "%1$@|You were charged %2$@";
"PUSH_CHANNEL_MESSAGE_TEXT" = "%1$@|%2$@";
"PUSH_CHANNEL_MESSAGE_NOTEXT" = "%1$@|posted a message";

View File

@ -5,9 +5,18 @@
#import <ImageDCT/YuvConversion.h>
@interface ImageDCTTable : NSObject
- (instancetype _Nonnull)initWithQuality:(NSInteger)quality isChroma:(bool)isChroma;
- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data;
- (NSData * _Nonnull)serializedData;
@end
@interface ImageDCT : NSObject
- (instancetype _Nonnull)initWithQuality:(NSInteger)quality;
- (instancetype _Nonnull)initWithTable:(ImageDCTTable * _Nonnull)table;
- (void)forwardWithPixels:(uint8_t const * _Nonnull)pixels coefficients:(int16_t * _Nonnull)coefficients width:(NSInteger)width height:(NSInteger)height bytesPerRow:(NSInteger)bytesPerRow __attribute__((objc_direct));
- (void)inverseWithCoefficients:(int16_t const * _Nonnull)coefficients pixels:(uint8_t * _Nonnull)pixels width:(NSInteger)width height:(NSInteger)height coefficientsPerRow:(NSInteger)coefficientsPerRow bytesPerRow:(NSInteger)bytesPerRow __attribute__((objc_direct));

View File

@ -118,6 +118,16 @@ static DCTELEM std_luminance_quant_tbl[DCTSIZE2] = {
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99
};
static DCTELEM std_chrominance_quant_tbl[DCTSIZE2] = {
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99
};
int jpeg_quality_scaling(int quality)
/* Convert a user-specified quality rating to a percentage scaling factor
@ -143,7 +153,7 @@ int jpeg_quality_scaling(int quality)
return quality;
}
void jpeg_add_quant_table(DCTELEM *qtable, DCTELEM *basicTable, int scale_factor, bool forceBaseline)
void jpeg_add_quant_table(DCTELEM *qtable, DCTELEM const *basicTable, int scale_factor, bool forceBaseline)
/* Define a quantization table equal to the basic_table times
* a scale factor (given as a percentage).
* If force_baseline is TRUE, the computed quantization table entries
@ -164,7 +174,7 @@ void jpeg_add_quant_table(DCTELEM *qtable, DCTELEM *basicTable, int scale_factor
}
}
void jpeg_set_quality(DCTELEM *qtable, int quality)
void jpeg_set_quality(DCTELEM *qtable, DCTELEM const *basicTable, int quality)
/* Set or change the 'quality' (quantization) setting, using default tables.
* This is the standard quality-adjusting entry point for typical user
* interfaces; only those who want detailed control over quantization tables
@ -175,10 +185,10 @@ void jpeg_set_quality(DCTELEM *qtable, int quality)
quality = jpeg_quality_scaling(quality);
/* Set up standard quality tables */
jpeg_add_quant_table(qtable, std_luminance_quant_tbl, quality, false);
jpeg_add_quant_table(qtable, basicTable, quality, false);
}
void getDivisors(DCTELEM *dtbl, DCTELEM *qtable) {
void getDivisors(DCTELEM *dtbl, DCTELEM const *qtable) {
#define CONST_BITS 14
#define RIGHT_SHIFT(x, shft) ((x) >> (shft))
@ -234,22 +244,15 @@ void quantize(JCOEFPTR coef_block, DCTELEM *divisors, DCTELEM *workspace)
}
}
void generateForwardDctData(int quality, std::vector<uint8_t> &data) {
void generateForwardDctData(DCTELEM const *qtable, std::vector<uint8_t> &data) {
data.resize(DCTSIZE2 * 4 * sizeof(DCTELEM));
DCTELEM qtable[DCTSIZE2];
jpeg_set_quality(qtable, quality);
getDivisors((DCTELEM *)data.data(), qtable);
}
void generateInverseDctData(int quality, std::vector<uint8_t> &data) {
void generateInverseDctData(DCTELEM const *qtable, std::vector<uint8_t> &data) {
data.resize(DCTSIZE2 * sizeof(IFAST_MULT_TYPE));
IFAST_MULT_TYPE *ifmtbl = (IFAST_MULT_TYPE *)data.data();
DCTELEM qtable[DCTSIZE2];
jpeg_set_quality(qtable, quality);
#define CONST_BITS 14
static const int16_t aanscales[DCTSIZE2] = {
/* precomputed values scaled up by 14 bits */
@ -338,13 +341,32 @@ void performInverseDct(int16_t const * coefficients, uint8_t *pixels, int width,
namespace dct {
DCTTable DCTTable::generate(int quality, bool isChroma) {
DCTTable result;
result.table.resize(DCTSIZE2);
if (isChroma) {
jpeg_set_quality(result.table.data(), std_chrominance_quant_tbl, quality);
} else {
jpeg_set_quality(result.table.data(), std_luminance_quant_tbl, quality);
}
return result;
}
DCTTable DCTTable::initializeEmpty() {
DCTTable result;
result.table.resize(DCTSIZE2);
return result;
}
class DCTInternal {
public:
DCTInternal(int quality) {
DCTInternal(DCTTable const &dctTable) {
auxiliaryData = createDctAuxiliaryData();
generateForwardDctData(quality, forwardDctData);
generateInverseDctData(quality, inverseDctData);
generateForwardDctData(dctTable.table.data(), forwardDctData);
generateInverseDctData(dctTable.table.data(), inverseDctData);
}
~DCTInternal() {
@ -357,8 +379,8 @@ public:
std::vector<uint8_t> inverseDctData;
};
DCT::DCT(int quality) {
_internal = new DCTInternal(quality);
DCT::DCT(DCTTable const &dctTable) {
_internal = new DCTInternal(dctTable);
}
DCT::~DCT() {

View File

@ -3,15 +3,23 @@
#include "DCTCommon.h"
#include <vector>
#include <stdint.h>
namespace dct {
class DCTInternal;
struct DCTTable {
static DCTTable generate(int quality, bool isChroma);
static DCTTable initializeEmpty();
std::vector<int16_t> table;
};
class DCT {
public:
DCT(int quality);
DCT(DCTTable const &dctTable);
~DCT();
void forward(uint8_t const *pixels, int16_t *coefficients, int width, int height, int bytesPerRow);

View File

@ -4,6 +4,41 @@
#include "DCT.h"
@interface ImageDCTTable () {
@public
dct::DCTTable _table;
}
@end
@implementation ImageDCTTable
- (instancetype _Nonnull)initWithQuality:(NSInteger)quality isChroma:(bool)isChroma {
self = [super init];
if (self != nil) {
_table = dct::DCTTable::generate((int)quality, isChroma);
}
return self;
}
- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data {
self = [super init];
if (self != nil) {
_table = dct::DCTTable::initializeEmpty();
if (data.length != _table.table.size() * 2) {
return nil;
}
memcpy(_table.table.data(), data.bytes, data.length);
}
return self;
}
- (NSData * _Nonnull)serializedData {
return [[NSData alloc] initWithBytes:_table.table.data() length:_table.table.size() * 2];
}
@end
@interface ImageDCT () {
std::unique_ptr<dct::DCT> _dct;
}
@ -12,10 +47,10 @@
@implementation ImageDCT
- (instancetype _Nonnull)initWithQuality:(NSInteger)quality {
- (instancetype _Nonnull)initWithTable:(ImageDCTTable * _Nonnull)table {
self = [super init];
if (self != nil) {
_dct = std::unique_ptr<dct::DCT>(new dct::DCT((int)quality));
_dct = std::unique_ptr<dct::DCT>(new dct::DCT(table->_table));
}
return self;
}

View File

@ -294,10 +294,10 @@ private final class AnimationCacheItemWriterInternal {
}
let dctData: DctData
if let current = self.currentDctData, current.quality == self.dctQuality {
if let current = self.currentDctData {
dctData = current
} else {
dctData = DctData(quality: self.dctQuality)
dctData = DctData(generatingTablesAtQuality: self.dctQuality)
self.currentDctData = dctData
}
@ -310,11 +310,18 @@ private final class AnimationCacheItemWriterInternal {
yuvaSurface.dct(dctData: dctData, target: dctCoefficients)
if isFirstFrame {
self.file.write(3 as UInt32)
self.file.write(4 as UInt32)
self.file.write(UInt32(dctCoefficients.yPlane.width))
self.file.write(UInt32(dctCoefficients.yPlane.height))
self.file.write(UInt32(dctData.quality))
let lumaDctTable = dctData.lumaTable.serializedData()
self.file.write(UInt32(lumaDctTable.count))
let _ = self.file.write(lumaDctTable)
let chromaDctTable = dctData.chromaTable.serializedData()
self.file.write(UInt32(chromaDctTable.count))
let _ = self.file.write(chromaDctTable)
self.contentLengthOffset = Int(self.file.position())
self.file.write(0 as UInt32)
@ -501,10 +508,10 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
}
let dctData: DctData
if let current = self.currentDctData, current.quality == self.dctQuality {
if let current = self.currentDctData {
dctData = current
} else {
dctData = DctData(quality: self.dctQuality)
dctData = DctData(generatingTablesAtQuality: self.dctQuality)
self.currentDctData = dctData
}
@ -526,11 +533,18 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
yuvaSurface.dct(dctData: dctData, target: dctCoefficients)
if isFirstFrame {
file.write(3 as UInt32)
file.write(4 as UInt32)
file.write(UInt32(dctCoefficients.yPlane.width))
file.write(UInt32(dctCoefficients.yPlane.height))
file.write(UInt32(dctData.quality))
let lumaDctTable = dctData.lumaTable.serializedData()
file.write(UInt32(lumaDctTable.count))
let _ = file.write(lumaDctTable)
let chromaDctTable = dctData.chromaTable.serializedData()
file.write(UInt32(chromaDctTable.count))
let _ = file.write(chromaDctTable)
self.contentLengthOffset = Int(file.position())
file.write(0 as UInt32)
@ -652,10 +666,10 @@ private final class AnimationCacheItemAccessor {
private var currentFrame: CurrentFrame?
private var currentYUVASurface: ImageYUVA420?
private var currentDctData: DctData
private let currentDctData: DctData
private var sharedDctCoefficients: DctCoefficientsYUVA420?
init(data: Data, range: Range<Int>, frameMapping: [FrameInfo], width: Int, height: Int, dctQuality: Int) {
init(data: Data, range: Range<Int>, frameMapping: [FrameInfo], width: Int, height: Int, dctData: DctData) {
self.data = data
self.range = range
self.width = width
@ -673,7 +687,7 @@ private final class AnimationCacheItemAccessor {
self.frameMapping = resultFrameMapping
self.durationMapping = durationMapping
self.currentDctData = DctData(quality: dctQuality)
self.currentDctData = dctData
}
private func loadNextFrame() {
@ -835,6 +849,16 @@ private final class AnimationCacheItemAccessor {
}
}
private func readData(data: Data, offset: Int, count: Int) -> Data {
var result = Data(count: count)
result.withUnsafeMutableBytes { bytes -> Void in
data.withUnsafeBytes { dataBytes -> Void in
memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), count)
}
}
return result
}
private func readUInt32(data: Data, offset: Int) -> UInt32 {
var value: UInt32 = 0
withUnsafeMutableBytes(of: &value, { bytes -> Void in
@ -1071,7 +1095,7 @@ private func loadItem(path: String) throws -> AnimationCacheItem {
}
let formatVersion = readUInt32(data: compressedData, offset: offset)
offset += 4
if formatVersion != 3 {
if formatVersion != 4 {
throw LoadItemError.dataError
}
@ -1090,9 +1114,27 @@ private func loadItem(path: String) throws -> AnimationCacheItem {
if offset + 4 > dataLength {
throw LoadItemError.dataError
}
let dctQuality = readUInt32(data: compressedData, offset: offset)
let dctLumaTableLength = readUInt32(data: compressedData, offset: offset)
offset += 4
if offset + Int(dctLumaTableLength) > dataLength {
throw LoadItemError.dataError
}
let dctLumaData = readData(data: compressedData, offset: offset, count: Int(dctLumaTableLength))
offset += Int(dctLumaTableLength)
if offset + 4 > dataLength {
throw LoadItemError.dataError
}
let dctChromaTableLength = readUInt32(data: compressedData, offset: offset)
offset += 4
if offset + Int(dctChromaTableLength) > dataLength {
throw LoadItemError.dataError
}
let dctChromaData = readData(data: compressedData, offset: offset, count: Int(dctChromaTableLength))
offset += Int(dctChromaTableLength)
if offset + 4 > dataLength {
throw LoadItemError.dataError
}
@ -1119,7 +1161,11 @@ private func loadItem(path: String) throws -> AnimationCacheItem {
frameMapping.append(AnimationCacheItemAccessor.FrameInfo(duration: Double(frameDuration)))
}
let itemAccessor = AnimationCacheItemAccessor(data: compressedData, range: compressedFrameDataRange, frameMapping: frameMapping, width: Int(width), height: Int(height), dctQuality: Int(dctQuality))
guard let dctData = DctData(lumaTable: dctLumaData, chromaTable: dctChromaData) else {
throw LoadItemError.dataError
}
let itemAccessor = AnimationCacheItemAccessor(data: compressedData, range: compressedFrameDataRange, frameMapping: frameMapping, width: Int(width), height: Int(height), dctData: dctData)
return AnimationCacheItem(numFrames: frameMapping.count, advanceImpl: { advance, requestedFormat in
return itemAccessor.advance(advance: advance, requestedFormat: requestedFormat)
@ -1397,7 +1443,9 @@ public final class AnimationCacheImpl: AnimationCache {
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
if FileManager.default.fileExists(atPath: itemFirstFramePath) {
return try? loadItem(path: itemFirstFramePath)
if let item = try? loadItem(path: itemFirstFramePath) {
return item
}
}
if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) {

View File

@ -152,12 +152,33 @@ extension ImageYUVA420 {
}
final class DctData {
let quality: Int
let dct: ImageDCT
let lumaTable: ImageDCTTable
let lumaDct: ImageDCT
init(quality: Int) {
self.quality = quality
self.dct = ImageDCT(quality: quality)
let chromaTable: ImageDCTTable
let chromaDct: ImageDCT
init?(lumaTable: Data, chromaTable: Data) {
guard let lumaTableData = ImageDCTTable(data: lumaTable) else {
return nil
}
guard let chromaTableData = ImageDCTTable(data: chromaTable) else {
return nil
}
self.lumaTable = lumaTableData
self.lumaDct = ImageDCT(table: lumaTableData)
self.chromaTable = chromaTableData
self.chromaDct = ImageDCT(table: chromaTableData)
}
init(generatingTablesAtQuality quality: Int) {
self.lumaTable = ImageDCTTable(quality: quality, isChroma: false)
self.lumaDct = ImageDCT(table: self.lumaTable)
self.chromaTable = ImageDCTTable(quality: quality, isChroma: true)
self.chromaDct = ImageDCT(table: self.chromaTable)
}
}
@ -168,19 +189,24 @@ extension ImageYUVA420 {
for i in 0 ..< 4 {
let sourcePlane: ImagePlane
let targetPlane: DctCoefficientPlane
let isChroma: Bool
switch i {
case 0:
sourcePlane = self.yPlane
targetPlane = target.yPlane
isChroma = false
case 1:
sourcePlane = self.uPlane
targetPlane = target.uPlane
isChroma = true
case 2:
sourcePlane = self.vPlane
targetPlane = target.vPlane
isChroma = true
case 3:
sourcePlane = self.aPlane
targetPlane = target.aPlane
isChroma = false
default:
preconditionFailure()
}
@ -191,7 +217,8 @@ extension ImageYUVA420 {
targetPlane.data.withUnsafeMutableBytes { bytes in
let coefficients = bytes.baseAddress!.assumingMemoryBound(to: Int16.self)
dctData.dct.forward(withPixels: sourcePixels, coefficients: coefficients, width: sourcePlane.width, height: sourcePlane.height, bytesPerRow: sourcePlane.bytesPerRow)
let dct = isChroma ? dctData.chromaDct : dctData.lumaDct
dct.forward(withPixels: sourcePixels, coefficients: coefficients, width: sourcePlane.width, height: sourcePlane.height, bytesPerRow: sourcePlane.bytesPerRow)
}
}
}
@ -211,19 +238,24 @@ extension DctCoefficientsYUVA420 {
for i in 0 ..< 4 {
let sourcePlane: DctCoefficientPlane
let targetPlane: ImagePlane
let isChroma: Bool
switch i {
case 0:
sourcePlane = self.yPlane
targetPlane = target.yPlane
isChroma = false
case 1:
sourcePlane = self.uPlane
targetPlane = target.uPlane
isChroma = true
case 2:
sourcePlane = self.vPlane
targetPlane = target.vPlane
isChroma = true
case 3:
sourcePlane = self.aPlane
targetPlane = target.aPlane
isChroma = false
default:
preconditionFailure()
}
@ -234,7 +266,8 @@ extension DctCoefficientsYUVA420 {
targetPlane.data.withUnsafeMutableBytes { bytes in
let pixels = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
dctData.dct.inverse(withCoefficients: coefficients, pixels: pixels, width: sourcePlane.width, height: sourcePlane.height, coefficientsPerRow: targetPlane.width, bytesPerRow: targetPlane.bytesPerRow)
let dct = isChroma ? dctData.chromaDct : dctData.lumaDct
dct.inverse(withCoefficients: coefficients, pixels: pixels, width: sourcePlane.width, height: sourcePlane.height, coefficientsPerRow: targetPlane.width, bytesPerRow: targetPlane.bytesPerRow)
}
}
}

View File

@ -24,6 +24,16 @@ private func traceScrollView(view: UIView, point: CGPoint) -> (UIScrollView?, Bo
return (nil, true)
}
private func traceScrollViewUp(view: UIView) -> UIScrollView? {
if let scrollView = view as? UIScrollView {
return scrollView
} else if let superview = view.superview {
return traceScrollViewUp(view: superview)
} else {
return nil
}
}
private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
enum LockDirection {
case up
@ -87,8 +97,8 @@ private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecogn
if let _ = hitView as? UIButton {
} else if let hitView = hitView, hitView.asyncdisplaykit_node is ASButtonNode {
} else {
if let scrollView = traceScrollView(view: view, point: point).0 {
if scrollView is ListViewScroller || scrollView is GridNodeScrollerView {
if let scrollView = traceScrollView(view: view, point: point).0 ?? hitView.flatMap(traceScrollViewUp) {
if scrollView is ListViewScroller || scrollView is GridNodeScrollerView || scrollView.asyncdisplaykit_node is ASScrollNode {
found = false
} else {
found = true

View File

@ -78,6 +78,7 @@ public final class EntityKeyboardComponent: Component {
public let emojiContent: EmojiPagerContentComponent
public let stickerContent: EmojiPagerContentComponent?
public let gifContent: GifPagerContentComponent?
public let hasRecentGifs: Bool
public let availableGifSearchEmojies: [GifSearchEmoji]
public let defaultToEmojiTab: Bool
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
@ -96,6 +97,7 @@ public final class EntityKeyboardComponent: Component {
emojiContent: EmojiPagerContentComponent,
stickerContent: EmojiPagerContentComponent?,
gifContent: GifPagerContentComponent?,
hasRecentGifs: Bool,
availableGifSearchEmojies: [GifSearchEmoji],
defaultToEmojiTab: Bool,
externalTopPanelContainer: PagerExternalTopPanelContainer?,
@ -113,6 +115,7 @@ public final class EntityKeyboardComponent: Component {
self.emojiContent = emojiContent
self.stickerContent = stickerContent
self.gifContent = gifContent
self.hasRecentGifs = hasRecentGifs
self.availableGifSearchEmojies = availableGifSearchEmojies
self.defaultToEmojiTab = defaultToEmojiTab
self.externalTopPanelContainer = externalTopPanelContainer
@ -142,6 +145,9 @@ public final class EntityKeyboardComponent: Component {
if lhs.gifContent != rhs.gifContent {
return false
}
if lhs.hasRecentGifs != rhs.hasRecentGifs {
return false
}
if lhs.availableGifSearchEmojies != rhs.availableGifSearchEmojies {
return false
}
@ -219,18 +225,20 @@ public final class EntityKeyboardComponent: Component {
contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(gifContent)))
var topGifItems: [EntityKeyboardTopPanelComponent.Item] = []
//TODO:localize
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: "recent",
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: "Chat/Input/Media/RecentTabIcon",
theme: component.theme,
title: "Recent",
pressed: { [weak self] in
self?.component?.switchToGifSubject(.recent)
}
if component.hasRecentGifs {
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: "recent",
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
imageName: "Chat/Input/Media/RecentTabIcon",
theme: component.theme,
title: "Recent",
pressed: { [weak self] in
self?.component?.switchToGifSubject(.recent)
}
))
))
))
}
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
id: "trending",
isReorderable: false,

View File

@ -437,6 +437,7 @@ final class EntityKeyboardStaticStickersPanelComponent: Component {
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollViewContainer.addSubview(self.scrollView)
@ -1114,6 +1115,7 @@ final class EntityKeyboardTopPanelComponent: Component {
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.addSubview(self.scrollView)

View File

@ -439,6 +439,7 @@ public final class GifPagerContentComponent: Component {
}
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.addSubview(self.scrollView)

View File

@ -107,27 +107,6 @@ private final class ItemAnimationContext {
data.withUnsafeBytes { bytes -> Void in
memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow)
/*var sourceBuffer = vImage_Buffer()
sourceBuffer.width = UInt(width)
sourceBuffer.height = UInt(height)
sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound))
sourceBuffer.rowBytes = bytesPerRow
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(32)
destinationBuffer.height = UInt(32)
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = bytesPerRow
vImageBoxConvolve_ARGB8888(&sourceBuffer,
&destinationBuffer,
nil,
UInt(width - 32 - 16), UInt(height - 32 - 16),
UInt32(31),
UInt32(31),
nil,
vImage_Flags(kvImageEdgeExtend))*/
}
guard let image = context.generateImage() else {
@ -184,42 +163,6 @@ private final class ItemAnimationContext {
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = context.bytesPerRow
/*var sourceBuffer = vImage_Buffer()
sourceBuffer.width = UInt(width)
sourceBuffer.height = UInt(height)
sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!)
sourceBuffer.rowBytes = bytesPerRow
let tempBufferBytes = malloc(blurredHeight * context.bytesPerRow)
defer {
free(tempBufferBytes)
}
let temp2BufferBytes = malloc(blurredHeight * context.bytesPerRow)
defer {
free(temp2BufferBytes)
}
memset(temp2BufferBytes, Int32(bitPattern: color?.argb ?? 0xffffffff), blurredHeight * context.bytesPerRow)
var tempBuffer = vImage_Buffer()
tempBuffer.width = UInt(blurredWidth)
tempBuffer.height = UInt(blurredHeight)
tempBuffer.data = tempBufferBytes
tempBuffer.rowBytes = context.bytesPerRow
var temp2Buffer = vImage_Buffer()
temp2Buffer.width = UInt(blurredWidth)
temp2Buffer.height = UInt(blurredHeight)
temp2Buffer.data = temp2BufferBytes
temp2Buffer.rowBytes = context.bytesPerRow
vImageScale_ARGB8888(&sourceBuffer, &tempBuffer, nil, vImage_Flags(kvImageDoNotTile))
//vImageUnpremultiplyData_ARGB8888(&tempBuffer, &tempBuffer, vImage_Flags(kvImageDoNotTile))
vImagePremultipliedAlphaBlend_ARGB8888(&tempBuffer, &temp2Buffer, &destinationBuffer, vImage_Flags(kvImageDoNotTile))
//vImageCopyBuffer(&tempBuffer, &destinationBuffer, 4, vImage_Flags(kvImageDoNotTile))*/
vImageBoxConvolve_ARGB8888(&destinationBuffer,
&destinationBuffer,
nil,
@ -302,14 +245,6 @@ private final class ItemAnimationContext {
}
strongSelf.item = result.item
strongSelf.updateIsPlaying()
if result.item == nil {
for target in strongSelf.targets.copyItems() {
if let target = target.value {
target.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
}
}
})
}

View File

@ -23,6 +23,7 @@ import ContextUI
import GalleryUI
import AttachmentTextInputPanelNode
import TelegramPresentationData
import TelegramNotices
private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
@ -45,17 +46,37 @@ private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment,
return result
}()
final class EntityKeyboardGifContent: Equatable {
let hasRecentGifs: Bool
let component: GifPagerContentComponent
init(hasRecentGifs: Bool, component: GifPagerContentComponent) {
self.hasRecentGifs = hasRecentGifs
self.component = component
}
static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool {
if lhs.hasRecentGifs != rhs.hasRecentGifs {
return false
}
if lhs.component != rhs.component {
return false
}
return true
}
}
final class ChatEntityKeyboardInputNode: ChatInputNode {
struct InputData: Equatable {
var emoji: EmojiPagerContentComponent
var stickers: EmojiPagerContentComponent?
var gifs: GifPagerContentComponent?
var gifs: EntityKeyboardGifContent?
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
init(
emoji: EmojiPagerContentComponent,
stickers: EmojiPagerContentComponent?,
gifs: GifPagerContentComponent?,
gifs: EntityKeyboardGifContent?,
availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
) {
self.emoji = emoji
@ -88,8 +109,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
var supergroupId: AnyHashable
var id: AnyHashable
var title: String
var subtitle: String?
var isPremiumLocked: Bool
var isFeatured: Bool
var isExpandable: Bool
var items: [EmojiPagerContentComponent.Item]
}
var itemGroups: [ItemGroup] = []
@ -134,7 +157,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} else {
itemGroupIndexById[groupId] = itemGroups.count
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Recently Used", isPremiumLocked: false, isFeatured: false, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Recently Used", subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, items: [resultItem]))
}
}
}
@ -153,7 +176,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} else {
itemGroupIndexById[groupId] = itemGroups.count
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Emoji", isPremiumLocked: false, isFeatured: false, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Emoji", subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, items: [resultItem]))
}
}
}
@ -191,7 +214,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
break inner
}
}
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, isPremiumLocked: isPremiumLocked, isFeatured: false, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, isExpandable: false, items: [resultItem]))
}
}
@ -217,7 +240,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, isPremiumLocked: isPremiumLocked, isFeatured: true, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, isExpandable: true, items: [resultItem]))
}
}
}
@ -234,7 +257,20 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
hasClear = true
}
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: group.title, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, hasClear: hasClear, displayPremiumBadges: false, items: group.items)
return EmojiPagerContentComponent.ItemGroup(
supergroupId: group.supergroupId,
groupId: group.id,
title: group.title,
subtitle: group.subtitle,
actionButtonTitle: nil,
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: false,
hasClear: hasClear,
isExpandable: group.isExpandable,
displayPremiumBadges: false,
items: group.items
)
},
itemLayoutType: .compact
)
@ -256,7 +292,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|> distinctUntilChanged
let emojiInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak interfaceInteraction, weak controllerInteraction] item, _, _, _ in
performItemAction: { [weak interfaceInteraction, weak controllerInteraction] _, item, _, _, _ in
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
return
@ -397,12 +433,44 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
chatPeerId: chatPeerId
)
let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak controllerInteraction, weak interfaceInteraction] item, view, rect, layer in
performItemAction: { [weak controllerInteraction, weak interfaceInteraction] groupId, item, view, rect, layer in
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
return
}
if let file = item.file {
guard let file = item.file else {
return
}
if groupId == AnyHashable("featuredTop") {
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controllerInteraction] views in
guard let controllerInteraction = controllerInteraction else {
return
}
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) {
controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen(
context: context,
highlightedPackId: featuredStickerPack.info.id,
sendSticker: { [weak controllerInteraction] fileReference, sourceNode, sourceRect in
guard let controllerInteraction = controllerInteraction else {
return false
}
return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil)
}
))
break
}
}
})
} else {
if file.isPremiumSticker && !hasPremium {
let controller = PremiumIntroScreen(context: context, source: .stickers)
controllerInteraction.navigationController()?.pushViewController(controller)
@ -427,7 +495,52 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
controller.navigationPresentation = .modal
controllerInteraction.navigationController()?.pushViewController(controller)
},
addGroupAction: { _, _ in
addGroupAction: { groupId, isPremiumLocked in
guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else {
return
}
if isPremiumLocked {
let controller = PremiumIntroScreen(context: context, source: .stickers)
controllerInteraction.navigationController()?.pushViewController(controller)
return
}
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredStickerPack.info.id == collectionId {
//let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start()
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case let .result(info, items, installed):
if installed {
return .complete()
} else {
return context.engine.stickers.addStickerPackInteractively(info: info, items: items)
}
case .fetching:
break
case .none:
break
}
return .complete()
}
|> deliverOnMainQueue).start(completed: {
})
break
}
}
})
},
clearGroup: { [weak controllerInteraction] groupId in
guard let controllerInteraction = controllerInteraction else {
@ -447,6 +560,20 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
})
])])
controllerInteraction.presentController(actionSheet, nil)
} else if groupId == AnyHashable("featuredTop") {
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
var stickerPackIds: [Int64] = []
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
stickerPackIds.append(featuredStickerPack.info.id.id)
}
let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: stickerPackIds).start()
})
}
},
pushController: { [weak controllerInteraction] controller in
@ -494,16 +621,22 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
let stickerItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000),
hasPremium,
context.account.viewTracker.featuredStickerPacks()
context.account.viewTracker.featuredStickerPacks(),
context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))),
ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager)
)
|> map { view, hasPremium, featuredStickerPacks -> EmojiPagerContentComponent in
|> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks -> EmojiPagerContentComponent in
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var title: String
var subtitle: String?
var actionButtonTitle: String?
var isPremiumLocked: Bool
var isFeatured: Bool
var displayPremiumBadges: Bool
@ -528,6 +661,50 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
var installedCollectionIds = Set<ItemCollectionId>()
for (id, _, _) in view.collectionInfos {
installedCollectionIds.insert(id)
}
let dismissedTrendingStickerPacksSet = Set(dismissedTrendingStickerPacks ?? [])
let featuredStickerPacksSet = Set(featuredStickerPacks.map(\.info.id.id))
if dismissedTrendingStickerPacksSet != featuredStickerPacksSet {
let featuredStickersConfiguration = featuredStickersConfiguration?.get(FeaturedStickersConfiguration.self)
for featuredStickerPack in featuredStickerPacks {
if installedCollectionIds.contains(featuredStickerPack.info.id) {
continue
}
guard let item = featuredStickerPack.topItems.first else {
continue
}
let resultItem = EmojiPagerContentComponent.Item(
file: item.file,
staticEmoji: nil,
subgroupId: nil
)
let supergroupId = "featuredTop"
let groupId: AnyHashable = supergroupId
let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium
if isPremiumLocked && isPremiumDisabled {
continue
}
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
let trendingIsPremium = featuredStickersConfiguration?.isPremium ?? false
let title = trendingIsPremium ? strings.Stickers_TrendingPremiumStickers : strings.StickerPacksSettings_FeaturedPacks
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
}
}
}
if let savedStickers = savedStickers {
for item in savedStickers.items {
guard let item = item.contents.get(SavedStickerItem.self) else {
@ -549,13 +726,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} else {
itemGroupIndexById[groupId] = itemGroups.count
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Saved", isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Saved", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
}
}
}
if let recentStickers = recentStickers {
var count = 0
for item in recentStickers.items {
guard let item = item.contents.get(RecentMediaItem.self) else {
continue
@ -576,12 +752,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} else {
itemGroupIndexById[groupId] = itemGroups.count
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Recently Used", isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
}
count += 1
if count >= 5 {
break
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Recently Used", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
}
}
}
@ -626,16 +797,11 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
} else {
itemGroupIndexById[groupId] = itemGroups.count
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Premium", isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Premium", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem]))
}
}
}
var installedCollectionIds = Set<ItemCollectionId>()
for (id, _, _) in view.collectionInfos {
installedCollectionIds.insert(id)
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
@ -658,7 +824,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
break inner
}
}
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, items: [resultItem]))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, items: [resultItem]))
}
}
@ -684,7 +850,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, items: [resultItem]))
let subtitle: String = strings.StickerPack_StickerCount(Int32(featuredStickerPack.info.count))
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, subtitle: subtitle, actionButtonTitle: strings.Stickers_Install, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, items: [resultItem]))
}
}
}
@ -697,11 +866,28 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
inputInteraction: stickerInputInteraction,
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var hasClear = false
var isEmbedded = false
if group.id == AnyHashable("recent") {
hasClear = true
} else if group.id == AnyHashable("featuredTop") {
hasClear = true
isEmbedded = true
}
return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: group.title, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, hasClear: hasClear, displayPremiumBadges: group.displayPremiumBadges, items: group.items)
return EmojiPagerContentComponent.ItemGroup(
supergroupId: group.supergroupId,
groupId: group.id,
title: group.title,
subtitle: group.subtitle,
actionButtonTitle: group.actionButtonTitle,
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: isEmbedded,
hasClear: hasClear,
isExpandable: false,
displayPremiumBadges: group.displayPremiumBadges,
items: group.items
)
},
itemLayoutType: .detailed
)
@ -752,16 +938,18 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
)
// We are going to subscribe to the actual data when the view is loaded
let gifItems: Signal<GifPagerContentComponent, NoError> = .single(GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: .recent,
items: [],
isLoading: false,
loadMoreToken: nil
let gifItems: Signal<EntityKeyboardGifContent, NoError> = .single(EntityKeyboardGifContent(
hasRecentGifs: true,
component: GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: .recent,
items: [],
isLoading: false,
loadMoreToken: nil
)
))
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
return combineLatest(queue: .mainQueue(),
emojiItems,
stickerItems,
@ -822,6 +1010,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
private let defaultToEmojiTab: Bool
private var currentInputData: InputData
private var inputDataDisposable: Disposable?
private var hasRecentGifsDisposable: Disposable?
private let controllerInteraction: ChatControllerInteraction?
@ -839,10 +1028,11 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
var switchToTextInput: (() -> Void)?
private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)?
private var scheduledInnerTransition: Transition?
private var gifMode: GifPagerContentComponent.Subject = .recent {
private var gifMode: GifPagerContentComponent.Subject? {
didSet {
if self.gifMode != oldValue {
if let gifMode = self.gifMode, gifMode != oldValue {
self.reloadGifContext()
}
}
@ -858,17 +1048,17 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
private final class GifContext {
private var componentValue: GifPagerContentComponent? {
private var componentValue: EntityKeyboardGifContent? {
didSet {
if let componentValue = self.componentValue {
self.componentResult.set(.single(componentValue))
}
}
}
private let componentPromise = Promise<GifPagerContentComponent>()
private let componentPromise = Promise<EntityKeyboardGifContent>()
private let componentResult = Promise<GifPagerContentComponent>()
var component: Signal<GifPagerContentComponent, NoError> {
private let componentResult = Promise<EntityKeyboardGifContent>()
var component: Signal<EntityKeyboardGifContent, NoError> {
return self.componentResult.get()
}
private var componentDisposable: Disposable?
@ -884,29 +1074,37 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
self.subject = subject
self.gifInputInteraction = gifInputInteraction
let gifItems: Signal<GifPagerContentComponent, NoError>
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|> map { savedGifs -> Bool in
return !savedGifs.isEmpty
}
let gifItems: Signal<EntityKeyboardGifContent, NoError>
switch subject {
case .recent:
gifItems = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|> map { savedGifs -> GifPagerContentComponent in
|> map { savedGifs -> EntityKeyboardGifContent in
var items: [GifPagerContentComponent.Item] = []
for gifItem in savedGifs {
items.append(GifPagerContentComponent.Item(
file: gifItem.contents.get(RecentMediaItem.self)!.media
))
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: false,
loadMoreToken: nil
return EntityKeyboardGifContent(
hasRecentGifs: true,
component: GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: false,
loadMoreToken: nil
)
)
}
case .trending:
gifItems = trendingGifs
|> map { trendingGifs -> GifPagerContentComponent in
gifItems = combineLatest(hasRecentGifs, trendingGifs)
|> map { hasRecentGifs, trendingGifs -> EntityKeyboardGifContent in
var items: [GifPagerContentComponent.Item] = []
var isLoading = false
@ -920,18 +1118,21 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
isLoading = true
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: isLoading,
loadMoreToken: nil
return EntityKeyboardGifContent(
hasRecentGifs: hasRecentGifs,
component: GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: isLoading,
loadMoreToken: nil
)
)
}
case let .emojiSearch(query):
gifItems = paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil)
|> map { result -> GifPagerContentComponent in
gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil))
|> map { hasRecentGifs, result -> EntityKeyboardGifContent in
var items: [GifPagerContentComponent.Item] = []
var loadMoreToken: String?
@ -947,13 +1148,16 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
isLoading = true
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: isLoading,
loadMoreToken: loadMoreToken
return EntityKeyboardGifContent(
hasRecentGifs: hasRecentGifs,
component: GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: isLoading,
loadMoreToken: loadMoreToken
)
)
}
}
@ -988,12 +1192,17 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
switch self.subject {
case let .emojiSearch(query):
let gifItems: Signal<GifPagerContentComponent, NoError>
gifItems = paneGifSearchForQuery(context: context, query: query, offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil)
|> map { result -> GifPagerContentComponent in
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|> map { savedGifs -> Bool in
return !savedGifs.isEmpty
}
let gifItems: Signal<EntityKeyboardGifContent, NoError>
gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query, offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil))
|> map { hasRecentGifs, result -> EntityKeyboardGifContent in
var items: [GifPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for item in componentValue.items {
for item in componentValue.component.items {
items.append(item)
existingIds.insert(item.file.fileId)
}
@ -1015,13 +1224,16 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
isLoading = true
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: isLoading,
loadMoreToken: loadMoreToken
return EntityKeyboardGifContent(
hasRecentGifs: hasRecentGifs,
component: GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
subject: subject,
items: items,
isLoading: isLoading,
loadMoreToken: loadMoreToken
)
)
}
@ -1038,7 +1250,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
}
private let gifComponent = Promise<GifPagerContentComponent>()
private let gifComponent = Promise<EntityKeyboardGifContent>()
private var gifInputInteraction: GifPagerContentComponent.InputInteraction?
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction?) {
@ -1059,6 +1271,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer()
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
updatedInputData,
self.gifComponent.get()
@ -1070,8 +1283,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
var inputData = inputData
inputData.gifs = gifs
var transition: Transition = .immediate
if strongSelf.currentInputData.emoji != inputData.emoji {
transition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(EmojiPagerContentComponent.ContentAnimation(type: .generic))
}
strongSelf.currentInputData = inputData
strongSelf.performLayout()
strongSelf.performLayout(transition: transition)
})
self.inputNodeInteraction = ChatMediaInputNodeInteraction(
@ -1137,16 +1354,35 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
self.reloadGifContext()
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|> map { savedGifs -> Bool in
return !savedGifs.isEmpty
}
self.hasRecentGifsDisposable = (hasRecentGifs
|> deliverOnMainQueue).start(next: { [weak self] hasRecentGifs in
guard let strongSelf = self else {
return
}
if let gifMode = strongSelf.gifMode {
if !hasRecentGifs, case .recent = gifMode {
strongSelf.gifMode = .trending
}
} else {
strongSelf.gifMode = hasRecentGifs ? .recent : .trending
}
})
}
deinit {
self.inputDataDisposable?.dispose()
self.hasRecentGifsDisposable?.dispose()
}
private func reloadGifContext() {
if let gifInputInteraction = self.gifInputInteraction {
self.gifContext = GifContext(context: self.context, subject: self.gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get())
if let gifInputInteraction = self.gifInputInteraction, let gifMode = self.gifMode {
self.gifContext = GifContext(context: self.context, subject: gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get())
}
}
@ -1154,16 +1390,25 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
self.isMarkInputCollapsed = true
}
private func performLayout() {
private func performLayout(transition: Transition) {
guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) = self.currentState else {
return
}
self.scheduledInnerTransition = transition
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded)
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded)
let innerTransition: Transition
if let scheduledInnerTransition = self.scheduledInnerTransition {
self.scheduledInnerTransition = nil
innerTransition = scheduledInnerTransition
} else {
innerTransition = Transition(transition)
}
let wasMarkedInputCollapsed = self.isMarkInputCollapsed
self.isMarkInputCollapsed = false
@ -1179,14 +1424,14 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let inputNodeInteraction = self.inputNodeInteraction!
let trendingGifsPromise = self.trendingGifsPromise
var mappedTransition = Transition(transition)
var mappedTransition = innerTransition
if wasMarkedInputCollapsed || !isExpanded {
mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed())
}
var stickerContent: EmojiPagerContentComponent? = self.currentInputData.stickers
var gifContent: GifPagerContentComponent? = self.currentInputData.gifs
var gifContent: EntityKeyboardGifContent? = self.currentInputData.gifs
var stickersEnabled = true
if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel {
@ -1204,12 +1449,6 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
gifContent = nil
}
if let gifContentValue = gifContent {
if gifContentValue.items.isEmpty {
gifContent = nil
}
}
let entityKeyboardSize = self.entityKeyboardView.update(
transition: mappedTransition,
component: AnyComponent(EntityKeyboardComponent(
@ -1217,7 +1456,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
bottomInset: bottomInset,
emojiContent: self.currentInputData.emoji,
stickerContent: stickerContent,
gifContent: gifContent,
gifContent: gifContent?.component,
hasRecentGifs: gifContent?.hasRecentGifs ?? false,
availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies,
defaultToEmojiTab: self.defaultToEmojiTab,
externalTopPanelContainer: self.externalTopPanelContainerImpl,
@ -1483,7 +1723,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
self.clipsToBounds = true
let inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] item, _, _, _ in
performItemAction: { [weak self] _, item, _, _, _ in
guard let strongSelf = self else {
return
}