mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Animation rendering
This commit is contained in:
parent
3d06cbf4a4
commit
591cc53c67
@ -212,14 +212,12 @@ using AS::MutexLocker;
|
|||||||
displayBlock = ^id{
|
displayBlock = ^id{
|
||||||
CHECK_CANCELLED_AND_RETURN_NIL();
|
CHECK_CANCELLED_AND_RETURN_NIL();
|
||||||
|
|
||||||
ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
|
UIImage *image = ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, ^{
|
||||||
|
for (dispatch_block_t block in displayBlocks) {
|
||||||
for (dispatch_block_t block in displayBlocks) {
|
if (isCancelledBlock()) return;
|
||||||
CHECK_CANCELLED_AND_RETURN_NIL(ASGraphicsEndImageContext());
|
block();
|
||||||
block();
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
UIImage *image = ASGraphicsGetImageAndEndCurrentContext();
|
|
||||||
|
|
||||||
ASDN_DELAY_FOR_DISPLAY();
|
ASDN_DELAY_FOR_DISPLAY();
|
||||||
return image;
|
return image;
|
||||||
@ -228,38 +226,35 @@ using AS::MutexLocker;
|
|||||||
displayBlock = ^id{
|
displayBlock = ^id{
|
||||||
CHECK_CANCELLED_AND_RETURN_NIL();
|
CHECK_CANCELLED_AND_RETURN_NIL();
|
||||||
|
|
||||||
if (shouldCreateGraphicsContext) {
|
__block UIImage *image = nil;
|
||||||
ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
|
void (^workWithContext)() = ^{
|
||||||
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
|
CGContextRef currentContext = UIGraphicsGetCurrentContext();
|
||||||
}
|
|
||||||
|
if (shouldCreateGraphicsContext && !currentContext) {
|
||||||
CGContextRef currentContext = UIGraphicsGetCurrentContext();
|
ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
|
||||||
UIImage *image = nil;
|
return;
|
||||||
|
}
|
||||||
if (shouldCreateGraphicsContext && !currentContext) {
|
|
||||||
//ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
|
// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
|
||||||
return nil;
|
// _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs.
|
||||||
}
|
[self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters];
|
||||||
|
|
||||||
// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
|
if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly.
|
||||||
// _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs.
|
image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock];
|
||||||
[self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters];
|
} else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext.
|
||||||
|
[self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
|
||||||
if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly.
|
}
|
||||||
image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock];
|
|
||||||
} else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext.
|
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
|
||||||
[self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
|
ASDN_DELAY_FOR_DISPLAY();
|
||||||
}
|
};
|
||||||
|
|
||||||
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
|
if (shouldCreateGraphicsContext) {
|
||||||
|
return ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, workWithContext);
|
||||||
if (shouldCreateGraphicsContext) {
|
} else {
|
||||||
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
|
workWithContext();
|
||||||
image = ASGraphicsGetImageAndEndCurrentContext();
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
ASDN_DELAY_FOR_DISPLAY();
|
|
||||||
return image;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,9 +304,6 @@ using AS::MutexLocker;
|
|||||||
}
|
}
|
||||||
|
|
||||||
__instanceLock__.lock();
|
__instanceLock__.lock();
|
||||||
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
|
|
||||||
CGFloat cornerRadius = _cornerRadius;
|
|
||||||
CGFloat contentsScale = _contentsScaleForDisplay;
|
|
||||||
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
|
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
|
||||||
__instanceLock__.unlock();
|
__instanceLock__.unlock();
|
||||||
|
|
||||||
@ -320,48 +312,6 @@ using AS::MutexLocker;
|
|||||||
didDisplayNodeContentWithRenderingContext(context, drawParameters);
|
didDisplayNodeContentWithRenderingContext(context, drawParameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0f) {
|
|
||||||
CGRect bounds = CGRectZero;
|
|
||||||
if (context == NULL) {
|
|
||||||
bounds = self.threadSafeBounds;
|
|
||||||
bounds.size.width *= contentsScale;
|
|
||||||
bounds.size.height *= contentsScale;
|
|
||||||
CGFloat white = 0.0f, alpha = 0.0f;
|
|
||||||
[backgroundColor getWhite:&white alpha:&alpha];
|
|
||||||
ASGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale);
|
|
||||||
[*image drawInRect:bounds];
|
|
||||||
} else {
|
|
||||||
bounds = CGContextGetClipBoundingBox(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self);
|
|
||||||
|
|
||||||
UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds];
|
|
||||||
[roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]];
|
|
||||||
roundedHole.usesEvenOddFillRule = YES;
|
|
||||||
|
|
||||||
UIBezierPath *roundedPath = nil;
|
|
||||||
if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0
|
|
||||||
CGFloat strokeThickness = borderWidth * contentsScale;
|
|
||||||
CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f;
|
|
||||||
roundedPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(bounds, strokeInset, strokeInset)
|
|
||||||
cornerRadius:_cornerRadius * contentsScale];
|
|
||||||
roundedPath.lineWidth = strokeThickness;
|
|
||||||
[[UIColor colorWithCGColor:borderColor] setStroke];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Punch out the corners by copying the backgroundColor over them.
|
|
||||||
// This works for everything from clearColor to opaque colors.
|
|
||||||
[backgroundColor setFill];
|
|
||||||
[roundedHole fillWithBlendMode:kCGBlendModeCopy alpha:1.0f];
|
|
||||||
|
|
||||||
[roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil.
|
|
||||||
|
|
||||||
if (*image) {
|
|
||||||
*image = ASGraphicsGetImageAndEndCurrentContext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously
|
- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously
|
||||||
|
@ -1539,41 +1539,6 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor
|
|
||||||
{
|
|
||||||
ASPerformBlockOnMainThread(^{
|
|
||||||
for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) {
|
|
||||||
// Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left.
|
|
||||||
// anchorPoint is Bottom Left at 0,0 and Top Right at 1,1.
|
|
||||||
BOOL isTop = (idx == 0 || idx == 1);
|
|
||||||
BOOL isRight = (idx == 1 || idx == 2);
|
|
||||||
|
|
||||||
CGSize size = CGSizeMake(radius + 1, radius + 1);
|
|
||||||
ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay);
|
|
||||||
|
|
||||||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
||||||
if (isRight == YES) {
|
|
||||||
CGContextTranslateCTM(ctx, -radius + 1, 0);
|
|
||||||
}
|
|
||||||
if (isTop == YES) {
|
|
||||||
CGContextTranslateCTM(ctx, 0, -radius + 1);
|
|
||||||
}
|
|
||||||
UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius];
|
|
||||||
[roundedRect setUsesEvenOddFillRule:YES];
|
|
||||||
[roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]];
|
|
||||||
[backgroundColor setFill];
|
|
||||||
[roundedRect fill];
|
|
||||||
|
|
||||||
// No lock needed, as _clipCornerLayers is only modified on the main thread.
|
|
||||||
CALayer *clipCornerLayer = _clipCornerLayers[idx];
|
|
||||||
clipCornerLayer.contents = (id)(ASGraphicsGetImageAndEndCurrentContext().CGImage);
|
|
||||||
clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height);
|
|
||||||
clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0);
|
|
||||||
}
|
|
||||||
[self _layoutClipCornersIfNeeded];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)_setClipCornerLayersVisible:(BOOL)visible
|
- (void)_setClipCornerLayersVisible:(BOOL)visible
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -1634,7 +1599,6 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
|
|||||||
}
|
}
|
||||||
else if (newRoundingType == ASCornerRoundingTypeClipping) {
|
else if (newRoundingType == ASCornerRoundingTypeClipping) {
|
||||||
// Clip corners already exist, but the radius has changed.
|
// Clip corners already exist, but the radius has changed.
|
||||||
[self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,161 +7,130 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import <AsyncDisplayKit/ASGraphicsContext.h>
|
#import <AsyncDisplayKit/ASGraphicsContext.h>
|
||||||
#import <AsyncDisplayKit/ASCGImageBuffer.h>
|
|
||||||
#import <AsyncDisplayKit/ASAssert.h>
|
#import <AsyncDisplayKit/ASAssert.h>
|
||||||
#import <AsyncDisplayKit/ASConfigurationInternal.h>
|
#import <AsyncDisplayKit/ASConfigurationInternal.h>
|
||||||
#import <AsyncDisplayKit/ASInternalHelpers.h>
|
#import <AsyncDisplayKit/ASInternalHelpers.h>
|
||||||
#import <UIKit/UIGraphics.h>
|
#import <AsyncDisplayKit/ASAvailability.h>
|
||||||
#import <UIKit/UIImage.h>
|
|
||||||
#import <objc/runtime.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Our version of the private CGBitmapGetAlignedBytesPerRow function.
|
|
||||||
*
|
|
||||||
* In both 32-bit and 64-bit, this function rounds up to nearest multiple of 32
|
|
||||||
* in iOS 9, 10, and 11. We'll try to catch if this ever changes by asserting that
|
|
||||||
* the bytes-per-row for a 1x1 context from the system is 32.
|
|
||||||
*/
|
|
||||||
static size_t ASGraphicsGetAlignedBytesPerRow(size_t baseValue) {
|
|
||||||
// Add 31 then zero out low 5 bits.
|
|
||||||
return (baseValue + 31) & ~0x1F;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
#if AS_AT_LEAST_IOS13
|
||||||
* A key used to associate CGContextRef -> NSMutableData, nonatomic retain.
|
#define ASPerformBlockWithTraitCollection(work, traitCollection) \
|
||||||
*
|
if (@available(iOS 13.0, tvOS 13.0, *)) { \
|
||||||
* That way the data will be released when the context dies. If they pull an image,
|
UITraitCollection *uiTraitCollection = ASPrimitiveTraitCollectionToUITraitCollection(traitCollection); \
|
||||||
* we will retain the data object (in a CGDataProvider) before releasing the context.
|
[uiTraitCollection performAsCurrentTraitCollection:^{ \
|
||||||
*/
|
work(); \
|
||||||
static UInt8 __contextDataAssociationKey;
|
}];\
|
||||||
|
} else { \
|
||||||
|
work(); \
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#define ASPerformBlockWithTraitCollection(work, traitCollection) work();
|
||||||
|
#endif
|
||||||
|
|
||||||
#pragma mark - Graphics Contexts
|
|
||||||
|
|
||||||
void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)
|
NS_AVAILABLE_IOS(10)
|
||||||
|
NS_INLINE void ASConfigureExtendedRange(UIGraphicsImageRendererFormat *format)
|
||||||
{
|
{
|
||||||
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
|
if (AS_AVAILABLE_IOS_TVOS(12, 12)) {
|
||||||
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
|
// nop. We always use automatic range on iOS >= 12.
|
||||||
return;
|
} else {
|
||||||
|
// Currently we never do wide color. One day we could pipe this information through from the ASImageNode if it was worth it.
|
||||||
|
format.prefersExtendedRange = NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use "reference contexts" to get device-specific options that UIKit
|
|
||||||
// uses.
|
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
static CGContextRef refCtxOpaque;
|
|
||||||
static CGContextRef refCtxTransparent;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 1);
|
|
||||||
refCtxOpaque = CGContextRetain(UIGraphicsGetCurrentContext());
|
|
||||||
ASDisplayNodeCAssert(CGBitmapContextGetBytesPerRow(refCtxOpaque) == 32, @"Expected bytes per row to be aligned to 32. Has CGBitmapGetAlignedBytesPerRow implementation changed?");
|
|
||||||
UIGraphicsEndImageContext();
|
|
||||||
|
|
||||||
// Make transparent ref context.
|
|
||||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1);
|
|
||||||
refCtxTransparent = CGContextRetain(UIGraphicsGetCurrentContext());
|
|
||||||
UIGraphicsEndImageContext();
|
|
||||||
});
|
|
||||||
|
|
||||||
// These options are taken from UIGraphicsBeginImageContext.
|
|
||||||
CGContextRef refCtx = opaque ? refCtxOpaque : refCtxTransparent;
|
|
||||||
CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(refCtx);
|
|
||||||
|
|
||||||
if (scale == 0) {
|
|
||||||
scale = ASScreenScale();
|
|
||||||
}
|
|
||||||
size_t intWidth = (size_t)ceil(size.width * scale);
|
|
||||||
size_t intHeight = (size_t)ceil(size.height * scale);
|
|
||||||
size_t bitsPerComponent = CGBitmapContextGetBitsPerComponent(refCtx);
|
|
||||||
size_t bytesPerRow = CGBitmapContextGetBitsPerPixel(refCtx) * intWidth / 8;
|
|
||||||
bytesPerRow = ASGraphicsGetAlignedBytesPerRow(bytesPerRow);
|
|
||||||
size_t bufferSize = bytesPerRow * intHeight;
|
|
||||||
CGColorSpaceRef colorspace = CGBitmapContextGetColorSpace(refCtx);
|
|
||||||
|
|
||||||
// We create our own buffer, and wrap the context around that. This way we can prevent
|
|
||||||
// the copy that usually gets made when you form a CGImage from the context.
|
|
||||||
ASCGImageBuffer *buffer = [[ASCGImageBuffer alloc] initWithLength:bufferSize];
|
|
||||||
|
|
||||||
CGContextRef context = CGBitmapContextCreate(buffer.mutableBytes, intWidth, intHeight, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo);
|
|
||||||
|
|
||||||
// Transfer ownership of the data to the context. So that if the context
|
|
||||||
// is destroyed before we create an image from it, the data will be released.
|
|
||||||
objc_setAssociatedObject((__bridge id)context, &__contextDataAssociationKey, buffer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
||||||
|
|
||||||
// Set the CTM to account for iOS orientation & specified scale.
|
|
||||||
// If only we could use CGContextSetBaseCTM. It doesn't
|
|
||||||
// seem like there are any consequences for our use case
|
|
||||||
// but we'll be on the look out. The internet hinted that it
|
|
||||||
// affects shadowing but I tested and shadowing works.
|
|
||||||
CGContextTranslateCTM(context, 0, intHeight);
|
|
||||||
CGContextScaleCTM(context, scale, -scale);
|
|
||||||
|
|
||||||
// Save the state so we can restore it and recover our scale in GetImageAndEnd
|
|
||||||
CGContextSaveGState(context);
|
|
||||||
|
|
||||||
// Transfer context ownership to the UIKit stack.
|
|
||||||
UIGraphicsPushContext(context);
|
|
||||||
CGContextRelease(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext() NS_RETURNS_RETAINED
|
UIImage *ASGraphicsCreateImageWithOptions(CGSize size, BOOL opaque, CGFloat scale, UIImage *sourceImage,
|
||||||
|
asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled,
|
||||||
|
void (^NS_NOESCAPE work)())
|
||||||
{
|
{
|
||||||
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
|
return ASGraphicsCreateImage(ASPrimitiveTraitCollectionMakeDefault(), size, opaque, scale, sourceImage, isCancelled, work);
|
||||||
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
|
||||||
UIGraphicsEndImageContext();
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop the context and make sure we have one.
|
|
||||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
||||||
if (context == NULL) {
|
|
||||||
ASDisplayNodeCFailAssert(@"Can't end image context without having begun one.");
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the device-specific ICC-based color space to use for the image.
|
|
||||||
// For DeviceRGB contexts (e.g. UIGraphics), CGBitmapContextCreateImage
|
|
||||||
// generates an image in a device-specific color space (for wide color support).
|
|
||||||
// We replicate that behavior, even though at this time CA does not
|
|
||||||
// require the image to be in this space. Plain DeviceRGB images seem
|
|
||||||
// to be treated exactly the same, but better safe than sorry.
|
|
||||||
static CGColorSpaceRef imageColorSpace;
|
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0);
|
|
||||||
UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
||||||
imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage));
|
|
||||||
ASDisplayNodeCAssertNotNil(imageColorSpace, nil);
|
|
||||||
UIGraphicsEndImageContext();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retrieve our buffer and create a CGDataProvider from it.
|
|
||||||
ASCGImageBuffer *buffer = objc_getAssociatedObject((__bridge id)context, &__contextDataAssociationKey);
|
|
||||||
ASDisplayNodeCAssertNotNil(buffer, nil);
|
|
||||||
CGDataProviderRef provider = [buffer createDataProviderAndInvalidate];
|
|
||||||
|
|
||||||
// Create the CGImage. Options taken from CGBitmapContextCreateImage.
|
|
||||||
CGImageRef cgImg = CGImageCreate(CGBitmapContextGetWidth(context), CGBitmapContextGetHeight(context), CGBitmapContextGetBitsPerComponent(context), CGBitmapContextGetBitsPerPixel(context), CGBitmapContextGetBytesPerRow(context), imageColorSpace, CGBitmapContextGetBitmapInfo(context), provider, NULL, true, kCGRenderingIntentDefault);
|
|
||||||
CGDataProviderRelease(provider);
|
|
||||||
|
|
||||||
// We saved our GState right after setting the CTM so that we could restore it
|
|
||||||
// here and get the original scale back.
|
|
||||||
CGContextRestoreGState(context);
|
|
||||||
CGFloat scale = CGContextGetCTM(context).a;
|
|
||||||
|
|
||||||
// Note: popping from the UIKit stack will probably destroy the context.
|
|
||||||
context = NULL;
|
|
||||||
UIGraphicsPopContext();
|
|
||||||
|
|
||||||
UIImage *result = [[UIImage alloc] initWithCGImage:cgImg scale:scale orientation:UIImageOrientationUp];
|
|
||||||
CGImageRelease(cgImg);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ASGraphicsEndImageContext()
|
UIImage *ASGraphicsCreateImage(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * sourceImage, asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled, void (NS_NOESCAPE ^work)()) {
|
||||||
{
|
if (@available(iOS 10.0, *)) {
|
||||||
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
|
if (true /*ASActivateExperimentalFeature(ASExperimentalDrawingGlobal)*/) {
|
||||||
UIGraphicsEndImageContext();
|
// If they used default scale, reuse one of two preferred formats.
|
||||||
return;
|
static UIGraphicsImageRendererFormat *defaultFormat;
|
||||||
|
static UIGraphicsImageRendererFormat *opaqueFormat;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
|
||||||
|
defaultFormat = [UIGraphicsImageRendererFormat preferredFormat];
|
||||||
|
opaqueFormat = [UIGraphicsImageRendererFormat preferredFormat];
|
||||||
|
} else {
|
||||||
|
defaultFormat = [UIGraphicsImageRendererFormat defaultFormat];
|
||||||
|
opaqueFormat = [UIGraphicsImageRendererFormat defaultFormat];
|
||||||
|
}
|
||||||
|
opaqueFormat.opaque = YES;
|
||||||
|
ASConfigureExtendedRange(defaultFormat);
|
||||||
|
ASConfigureExtendedRange(opaqueFormat);
|
||||||
|
});
|
||||||
|
|
||||||
|
UIGraphicsImageRendererFormat *format;
|
||||||
|
if (sourceImage) {
|
||||||
|
if (sourceImage.renderingMode == UIImageRenderingModeAlwaysTemplate) {
|
||||||
|
// Template images will be black and transparent, so if we use
|
||||||
|
// sourceImage.imageRenderFormat it will assume a grayscale color space.
|
||||||
|
// This is not good because a template image should be able to tint to any color,
|
||||||
|
// so we'll just use the default here.
|
||||||
|
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
|
||||||
|
format = [UIGraphicsImageRendererFormat preferredFormat];
|
||||||
|
} else {
|
||||||
|
format = [UIGraphicsImageRendererFormat defaultFormat];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format = sourceImage.imageRendererFormat;
|
||||||
|
}
|
||||||
|
// We only want the private bits (color space and bits per component) from the image.
|
||||||
|
// We have our own ideas about opacity and scale.
|
||||||
|
format.opaque = opaque;
|
||||||
|
format.scale = scale;
|
||||||
|
} else if (scale == 0 || scale == ASScreenScale()) {
|
||||||
|
format = opaque ? opaqueFormat : defaultFormat;
|
||||||
|
} else {
|
||||||
|
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
|
||||||
|
format = [UIGraphicsImageRendererFormat preferredFormat];
|
||||||
|
} else {
|
||||||
|
format = [UIGraphicsImageRendererFormat defaultFormat];
|
||||||
|
}
|
||||||
|
if (opaque) format.opaque = YES;
|
||||||
|
format.scale = scale;
|
||||||
|
ASConfigureExtendedRange(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid using the imageWithActions: method because it does not support cancellation at the
|
||||||
|
// last moment i.e. before actually creating the resulting image.
|
||||||
|
__block UIImage *image;
|
||||||
|
NSError *error;
|
||||||
|
[[[UIGraphicsImageRenderer alloc] initWithSize:size format:format]
|
||||||
|
runDrawingActions:^(UIGraphicsImageRendererContext *rendererContext) {
|
||||||
|
ASDisplayNodeCAssert(UIGraphicsGetCurrentContext(), @"Should have a context!");
|
||||||
|
ASPerformBlockWithTraitCollection(work, traitCollection);
|
||||||
|
}
|
||||||
|
completionActions:^(UIGraphicsImageRendererContext *rendererContext) {
|
||||||
|
if (isCancelled == nil || !isCancelled()) {
|
||||||
|
image = rendererContext.currentImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error:&error];
|
||||||
|
if (error) {
|
||||||
|
NSCAssert(NO, @"Error drawing: %@", error);
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UIGraphicsPopContext();
|
// Bad OS or experiment flag. Use UIGraphics* API.
|
||||||
|
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
|
||||||
|
ASPerformBlockWithTraitCollection(work, traitCollection)
|
||||||
|
UIImage *image = nil;
|
||||||
|
if (isCancelled == nil || !isCancelled()) {
|
||||||
|
image = UIGraphicsGetImageFromCurrentImageContext();
|
||||||
|
}
|
||||||
|
UIGraphicsEndImageContext();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIImage *ASGraphicsCreateImageWithTraitCollectionAndOptions(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * sourceImage, void (NS_NOESCAPE ^work)()) {
|
||||||
|
return ASGraphicsCreateImage(traitCollection, size, opaque, scale, sourceImage, nil, work);
|
||||||
}
|
}
|
||||||
|
@ -6,46 +6,60 @@
|
|||||||
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
|
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
#import <AsyncDisplayKit/ASBaseDefines.h>
|
#import <AsyncDisplayKit/ASBaseDefines.h>
|
||||||
#import <CoreGraphics/CoreGraphics.h>
|
#import <AsyncDisplayKit/ASBlockTypes.h>
|
||||||
|
#import <AsyncDisplayKit/ASTraitCollection.h>
|
||||||
@class UIImage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functions for creating one-shot graphics contexts that do not have to copy
|
|
||||||
* their contents when an image is generated from them. This is efficient
|
|
||||||
* for our use, since we do not reuse graphics contexts.
|
|
||||||
*
|
|
||||||
* The API mirrors the UIGraphics API, with the exception that forming an image
|
|
||||||
* ends the context as well.
|
|
||||||
*
|
|
||||||
* Note: You must not mix-and-match between ASGraphics* and UIGraphics* functions
|
|
||||||
* within the same drawing operation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a one-shot context.
|
* A wrapper for the UIKit drawing APIs. If you are in ASExperimentalDrawingGlobal, and you have iOS >= 10, we will create
|
||||||
|
* a UIGraphicsRenderer with an appropriate format. Otherwise, we will use UIGraphicsBeginImageContext et al.
|
||||||
*
|
*
|
||||||
* Behavior is the same as UIGraphicsBeginImageContextWithOptions.
|
* @param size The size of the context.
|
||||||
|
* @param opaque Whether the context should be opaque or not.
|
||||||
|
* @param scale The scale of the context. 0 uses main screen scale.
|
||||||
|
* @param sourceImage If you are planning to render a UIImage into this context, provide it here and we will use its
|
||||||
|
* preferred renderer format if we are using UIGraphicsImageRenderer.
|
||||||
|
* @param isCancelled An optional block for canceling the drawing before forming the image. Only takes effect under
|
||||||
|
* the legacy code path, as UIGraphicsRenderer does not support cancellation.
|
||||||
|
* @param work A block, wherein the current UIGraphics context is set based on the arguments.
|
||||||
|
*
|
||||||
|
* @return The rendered image. You can also render intermediary images using UIGraphicsGetImageFromCurrentImageContext.
|
||||||
*/
|
*/
|
||||||
AS_EXTERN void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale);
|
UIImage *ASGraphicsCreateImageWithOptions(CGSize size, BOOL opaque, CGFloat scale, UIImage * _Nullable sourceImage, asdisplaynode_iscancelled_block_t NS_NOESCAPE _Nullable isCancelled, void (NS_NOESCAPE ^work)(void)) ASDISPLAYNODE_DEPRECATED_MSG("Use ASGraphicsCreateImageWithTraitCollectionAndOptions instead");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates and image and ends the current one-shot context.
|
* A wrapper for the UIKit drawing APIs. If you are in ASExperimentalDrawingGlobal, and you have iOS >= 10, we will create
|
||||||
*
|
* a UIGraphicsRenderer with an appropriate format. Otherwise, we will use UIGraphicsBeginImageContext et al.
|
||||||
* Behavior is the same as UIGraphicsGetImageFromCurrentImageContext followed by UIGraphicsEndImageContext.
|
*
|
||||||
*/
|
* @param traitCollection Trait collection. The `work` block will be executed with this trait collection, so it will affect dynamic colors, etc.
|
||||||
AS_EXTERN UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext(void) NS_RETURNS_RETAINED;
|
* @param size The size of the context.
|
||||||
|
* @param opaque Whether the context should be opaque or not.
|
||||||
|
* @param scale The scale of the context. 0 uses main screen scale.
|
||||||
|
* @param sourceImage If you are planning to render a UIImage into this context, provide it here and we will use its
|
||||||
|
* preferred renderer format if we are using UIGraphicsImageRenderer.
|
||||||
|
* @param isCancelled An optional block for canceling the drawing before forming the image.
|
||||||
|
* @param work A block, wherein the current UIGraphics context is set based on the arguments.
|
||||||
|
*
|
||||||
|
* @return The rendered image. You can also render intermediary images using UIGraphicsGetImageFromCurrentImageContext.
|
||||||
|
*/
|
||||||
|
UIImage *ASGraphicsCreateImage(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * _Nullable sourceImage, asdisplaynode_iscancelled_block_t _Nullable NS_NOESCAPE isCancelled, void (NS_NOESCAPE ^work)(void));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call this if you want to end the current context without making an image.
|
* A wrapper for the UIKit drawing APIs.
|
||||||
*
|
*
|
||||||
* Behavior is the same as UIGraphicsEndImageContext.
|
* @param traitCollection Trait collection. The `work` block will be executed with this trait collection, so it will affect dynamic colors, etc.
|
||||||
*/
|
* @param size The size of the context.
|
||||||
AS_EXTERN void ASGraphicsEndImageContext(void);
|
* @param opaque Whether the context should be opaque or not.
|
||||||
|
* @param scale The scale of the context. 0 uses main screen scale.
|
||||||
|
* @param sourceImage If you are planning to render a UIImage into this context, provide it here and we will use its
|
||||||
|
* preferred renderer format if we are using UIGraphicsImageRenderer.
|
||||||
|
* @param work A block, wherein the current UIGraphics context is set based on the arguments.
|
||||||
|
*
|
||||||
|
* @return The rendered image. You can also render intermediary images using UIGraphicsGetImageFromCurrentImageContext.
|
||||||
|
*/
|
||||||
|
UIImage *ASGraphicsCreateImageWithTraitCollectionAndOptions(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * _Nullable sourceImage, void (NS_NOESCAPE ^work)(void)) ASDISPLAYNODE_DEPRECATED_MSG("Use ASGraphicsCreateImage instead");
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
@ -18,7 +18,9 @@ public func authorizationCurrentOptionText(_ type: SentAuthorizationCodeType, st
|
|||||||
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: primaryColor)
|
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: primaryColor)
|
||||||
let bold = MarkdownAttributeSet(font: Font.semibold(16.0), textColor: primaryColor)
|
let bold = MarkdownAttributeSet(font: Font.semibold(16.0), textColor: primaryColor)
|
||||||
return parseMarkdownIntoAttributedString(strings.Login_ShortCallTitle, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center)
|
return parseMarkdownIntoAttributedString(strings.Login_ShortCallTitle, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center)
|
||||||
case .call, .flashCall:
|
case .call:
|
||||||
|
return NSAttributedString(string: strings.Login_CodeSentCall, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center)
|
||||||
|
case .flashCall:
|
||||||
return NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center)
|
return NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,7 +37,13 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType,
|
|||||||
let timeString = NSString(format: "%d:%.02d", Int(minutes), Int(seconds))
|
let timeString = NSString(format: "%d:%.02d", Int(minutes), Int(seconds))
|
||||||
return (NSAttributedString(string: strings.Login_WillSendSms(timeString as String).string, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
return (NSAttributedString(string: strings.Login_WillSendSms(timeString as String).string, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
||||||
}
|
}
|
||||||
case .call, .flashCall, .missedCall:
|
case .call:
|
||||||
|
if timeout <= 0 {
|
||||||
|
return (NSAttributedString(string: strings.Login_CodeSentCall, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
||||||
|
} else {
|
||||||
|
return (NSAttributedString(string: String(format: strings.ChangePhoneNumberCode_CallTimer(String(format: "%d:%.2d", minutes, seconds)).string, minutes, seconds), font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
||||||
|
}
|
||||||
|
case .flashCall, .missedCall:
|
||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
return (NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
return (NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
||||||
} else {
|
} else {
|
||||||
|
@ -606,7 +606,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
let wasVisible = self.visibilityStatus
|
let wasVisible = self.visibilityStatus
|
||||||
let isVisible: Bool
|
let isVisible: Bool
|
||||||
switch self.visibility {
|
switch self.visibility {
|
||||||
case let .visible(fraction):
|
case let .visible(fraction, _):
|
||||||
isVisible = fraction > 0.2
|
isVisible = fraction > 0.2
|
||||||
case .none:
|
case .none:
|
||||||
isVisible = false
|
isVisible = false
|
||||||
|
@ -492,7 +492,7 @@ public struct DeviceGraphicsContextSettings {
|
|||||||
public class DrawingContext {
|
public class DrawingContext {
|
||||||
public let size: CGSize
|
public let size: CGSize
|
||||||
public let scale: CGFloat
|
public let scale: CGFloat
|
||||||
private let scaledSize: CGSize
|
public let scaledSize: CGSize
|
||||||
public let bytesPerRow: Int
|
public let bytesPerRow: Int
|
||||||
private let bitmapInfo: CGBitmapInfo
|
private let bitmapInfo: CGBitmapInfo
|
||||||
public let length: Int
|
public let length: Int
|
||||||
@ -525,7 +525,7 @@ public class DrawingContext {
|
|||||||
f(self.context)
|
f(self.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false) {
|
public init(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil) {
|
||||||
assert(!size.width.isZero && !size.height.isZero)
|
assert(!size.width.isZero && !size.height.isZero)
|
||||||
let size: CGSize = CGSize(width: max(1.0, size.width), height: max(1.0, size.height))
|
let size: CGSize = CGSize(width: max(1.0, size.width), height: max(1.0, size.height))
|
||||||
|
|
||||||
@ -539,8 +539,8 @@ public class DrawingContext {
|
|||||||
self.scale = actualScale
|
self.scale = actualScale
|
||||||
self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale)
|
self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale)
|
||||||
|
|
||||||
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width))
|
self.bytesPerRow = bytesPerRow ?? DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width))
|
||||||
self.length = bytesPerRow * Int(scaledSize.height)
|
self.length = self.bytesPerRow * Int(scaledSize.height)
|
||||||
|
|
||||||
self.imageBuffer = ASCGImageBuffer(length: UInt(self.length))
|
self.imageBuffer = ASCGImageBuffer(length: UInt(self.length))
|
||||||
|
|
||||||
|
@ -3685,7 +3685,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
let itemContentFrame = itemNode.apparentContentFrame
|
let itemContentFrame = itemNode.apparentContentFrame
|
||||||
let intersection = itemContentFrame.intersection(visibilityRect)
|
let intersection = itemContentFrame.intersection(visibilityRect)
|
||||||
let fraction = intersection.height / itemContentFrame.height
|
let fraction = intersection.height / itemContentFrame.height
|
||||||
visibility = .visible(fraction)
|
|
||||||
|
let subRect = visibilityRect.intersection(itemFrame).offsetBy(dx: 0.0, dy: -itemFrame.minY)
|
||||||
|
|
||||||
|
visibility = .visible(fraction, subRect)
|
||||||
}
|
}
|
||||||
var updateVisibility = false
|
var updateVisibility = false
|
||||||
if !onlyPositive {
|
if !onlyPositive {
|
||||||
|
@ -49,24 +49,7 @@ public struct ListViewItemNodeLayout {
|
|||||||
|
|
||||||
public enum ListViewItemNodeVisibility: Equatable {
|
public enum ListViewItemNodeVisibility: Equatable {
|
||||||
case none
|
case none
|
||||||
case visible(CGFloat)
|
case visible(CGFloat, CGRect)
|
||||||
|
|
||||||
public static func ==(lhs: ListViewItemNodeVisibility, rhs: ListViewItemNodeVisibility) -> Bool {
|
|
||||||
switch lhs {
|
|
||||||
case .none:
|
|
||||||
if case .none = rhs {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case let .visible(fraction):
|
|
||||||
if case .visible(fraction) = rhs {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ListViewItemLayoutParams {
|
public struct ListViewItemLayoutParams {
|
||||||
|
@ -109,6 +109,14 @@ public final class ManagedFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func position() -> Int64 {
|
||||||
|
if let queue = self.queue {
|
||||||
|
assert(queue.isCurrent())
|
||||||
|
}
|
||||||
|
|
||||||
|
return lseek(self.fd, 0, SEEK_CUR);
|
||||||
|
}
|
||||||
|
|
||||||
public func sync() {
|
public func sync() {
|
||||||
if let queue = self.queue {
|
if let queue = self.queue {
|
||||||
assert(queue.isCurrent())
|
assert(queue.isCurrent())
|
||||||
@ -116,3 +124,39 @@ public final class ManagedFile {
|
|||||||
fsync(self.fd)
|
fsync(self.fd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension ManagedFile {
|
||||||
|
func write(_ data: Data) -> Int {
|
||||||
|
if data.isEmpty {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return data.withUnsafeBytes { bytes -> Int in
|
||||||
|
return self.write(bytes.baseAddress!, count: bytes.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(_ value: Int32) {
|
||||||
|
var value = value
|
||||||
|
let _ = self.write(&value, count: 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(_ value: UInt32) {
|
||||||
|
var value = value
|
||||||
|
let _ = self.write(&value, count: 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(_ value: Int64) {
|
||||||
|
var value = value
|
||||||
|
let _ = self.write(&value, count: 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(_ value: UInt64) {
|
||||||
|
var value = value
|
||||||
|
let _ = self.write(&value, count: 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(_ value: Float32) {
|
||||||
|
var value = value
|
||||||
|
let _ = self.write(&value, count: 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -275,6 +275,10 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent",
|
"//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent",
|
||||||
"//submodules/TelegramUI/Components/EditableChatTextNode:EditableChatTextNode",
|
"//submodules/TelegramUI/Components/EditableChatTextNode:EditableChatTextNode",
|
||||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||||
|
"//submodules/TelegramUI/Components/EmojiKeyboard:EmojiKeyboard",
|
||||||
|
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||||
|
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
|
||||||
|
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||||
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
|
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
|
||||||
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
|
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
|
||||||
] + select({
|
] + select({
|
||||||
|
20
submodules/TelegramUI/Components/AnimationCache/BUILD
Normal file
20
submodules/TelegramUI/Components/AnimationCache/BUILD
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "AnimationCache",
|
||||||
|
module_name = "AnimationCache",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
|
"//submodules/CryptoUtils:CryptoUtils",
|
||||||
|
"//submodules/ManagedFile:ManagedFile",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,406 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import CryptoUtils
|
||||||
|
import ManagedFile
|
||||||
|
|
||||||
|
public final class AnimationCacheItemFrame {
|
||||||
|
public enum Format {
|
||||||
|
case rgba(width: Int, height: Int, bytesPerRow: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
public let data: Data
|
||||||
|
public let range: Range<Int>
|
||||||
|
public let format: Format
|
||||||
|
public let duration: Double
|
||||||
|
|
||||||
|
public init(data: Data, range: Range<Int>, format: Format, duration: Double) {
|
||||||
|
self.data = data
|
||||||
|
self.range = range
|
||||||
|
self.format = format
|
||||||
|
self.duration = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class AnimationCacheItem {
|
||||||
|
public let numFrames: Int
|
||||||
|
private let getFrameImpl: (Int) -> AnimationCacheItemFrame?
|
||||||
|
|
||||||
|
public init(numFrames: Int, getFrame: @escaping (Int) -> AnimationCacheItemFrame?) {
|
||||||
|
self.numFrames = numFrames
|
||||||
|
self.getFrameImpl = getFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getFrame(index: Int) -> AnimationCacheItemFrame? {
|
||||||
|
return self.getFrameImpl(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol AnimationCacheItemWriter: AnyObject {
|
||||||
|
func add(bytes: UnsafeRawPointer, length: Int, width: Int, height: Int, bytesPerRow: Int, duration: Double)
|
||||||
|
func finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol AnimationCache: AnyObject {
|
||||||
|
func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal<AnimationCacheItem?, NoError>
|
||||||
|
}
|
||||||
|
|
||||||
|
private func md5Hash(_ string: String) -> String {
|
||||||
|
let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in
|
||||||
|
return CryptoMD5(bytes.baseAddress!, Int32(bytes.count))
|
||||||
|
}
|
||||||
|
return hashData.withUnsafeBytes { bytes -> String in
|
||||||
|
let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||||
|
return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func itemSubpath(hashString: String) -> (directory: String, fileName: String) {
|
||||||
|
assert(hashString.count == 32)
|
||||||
|
var directory = ""
|
||||||
|
|
||||||
|
for i in 0 ..< 1 {
|
||||||
|
if !directory.isEmpty {
|
||||||
|
directory.append("/")
|
||||||
|
}
|
||||||
|
directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (directory, hashString)
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
||||||
|
private struct ParameterSet: Equatable {
|
||||||
|
var width: Int
|
||||||
|
var height: Int
|
||||||
|
var bytesPerRow: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct FrameMetadata {
|
||||||
|
var offset: Int
|
||||||
|
var length: Int
|
||||||
|
var duration: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
private let file: ManagedFile
|
||||||
|
private let completion: (Bool) -> Void
|
||||||
|
|
||||||
|
private var currentParameterSet: ParameterSet?
|
||||||
|
private var contentLengthOffset: Int?
|
||||||
|
private var isFailed: Bool = false
|
||||||
|
private var isFinished: Bool = false
|
||||||
|
|
||||||
|
private var frames: [FrameMetadata] = []
|
||||||
|
private var contentLength: Int = 0
|
||||||
|
|
||||||
|
private let lock = Lock()
|
||||||
|
|
||||||
|
init?(tempPath: String, completion: @escaping (Bool) -> Void) {
|
||||||
|
guard let file = ManagedFile(queue: nil, path: tempPath, mode: .readwrite) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.file = file
|
||||||
|
self.completion = completion
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(bytes: UnsafeRawPointer, length: Int, width: Int, height: Int, bytesPerRow: Int, duration: Double) {
|
||||||
|
self.lock.locked {
|
||||||
|
if self.isFailed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let parameterSet = ParameterSet(width: width, height: height, bytesPerRow: bytesPerRow)
|
||||||
|
if let currentParameterSet = self.currentParameterSet {
|
||||||
|
if currentParameterSet != parameterSet {
|
||||||
|
self.isFailed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.currentParameterSet = parameterSet
|
||||||
|
|
||||||
|
self.file.write(1 as UInt32)
|
||||||
|
|
||||||
|
self.file.write(UInt32(parameterSet.width))
|
||||||
|
self.file.write(UInt32(parameterSet.height))
|
||||||
|
self.file.write(UInt32(parameterSet.bytesPerRow))
|
||||||
|
|
||||||
|
self.contentLengthOffset = Int(self.file.position())
|
||||||
|
self.file.write(0 as UInt32)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.frames.append(FrameMetadata(offset: Int(self.file.position()), length: length, duration: duration))
|
||||||
|
let _ = self.file.write(bytes, count: length)
|
||||||
|
self.contentLength += length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func finish() {
|
||||||
|
var shouldComplete = false
|
||||||
|
self.lock.locked {
|
||||||
|
if !self.isFinished {
|
||||||
|
self.isFinished = true
|
||||||
|
shouldComplete = true
|
||||||
|
|
||||||
|
guard let contentLengthOffset = self.contentLengthOffset else {
|
||||||
|
self.isFailed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadataPosition = self.file.position()
|
||||||
|
self.file.seek(position: Int64(contentLengthOffset))
|
||||||
|
self.file.write(UInt32(self.contentLength))
|
||||||
|
|
||||||
|
self.file.seek(position: metadataPosition)
|
||||||
|
self.file.write(UInt32(self.frames.count))
|
||||||
|
for frame in self.frames {
|
||||||
|
self.file.write(UInt32(frame.offset))
|
||||||
|
self.file.write(UInt32(frame.length))
|
||||||
|
self.file.write(Float32(frame.duration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldComplete {
|
||||||
|
self.completion(!self.isFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class AnimationCacheItemAccessor {
|
||||||
|
struct FrameInfo {
|
||||||
|
let range: Range<Int>
|
||||||
|
let duration: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
private let data: Data
|
||||||
|
private let frameMapping: [Int: FrameInfo]
|
||||||
|
private let format: AnimationCacheItemFrame.Format
|
||||||
|
|
||||||
|
init(data: Data, frameMapping: [Int: FrameInfo], format: AnimationCacheItemFrame.Format) {
|
||||||
|
self.data = data
|
||||||
|
self.frameMapping = frameMapping
|
||||||
|
self.format = format
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFrame(index: Int) -> AnimationCacheItemFrame? {
|
||||||
|
guard let frameInfo = self.frameMapping[index] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimationCacheItemFrame(data: data, range: frameInfo.range, format: self.format, duration: frameInfo.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func readUInt32(data: Data, offset: Int) -> UInt32 {
|
||||||
|
var value: UInt32 = 0
|
||||||
|
withUnsafeMutableBytes(of: &value, { bytes -> Void in
|
||||||
|
data.withUnsafeBytes { dataBytes -> Void in
|
||||||
|
memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadItem(path: String) -> AnimationCacheItem? {
|
||||||
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let dataLength = data.count
|
||||||
|
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
guard dataLength >= offset + 4 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let formatVersion = readUInt32(data: data, offset: offset)
|
||||||
|
offset += 4
|
||||||
|
if formatVersion != 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard dataLength >= offset + 4 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let width = readUInt32(data: data, offset: offset)
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
guard dataLength >= offset + 4 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let height = readUInt32(data: data, offset: offset)
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
guard dataLength >= offset + 4 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let bytesPerRow = readUInt32(data: data, offset: offset)
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
guard dataLength >= offset + 4 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let frameDataLength = readUInt32(data: data, offset: offset)
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
offset += Int(frameDataLength)
|
||||||
|
|
||||||
|
guard dataLength >= offset + 4 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let numFrames = readUInt32(data: data, offset: offset)
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
var frameMapping: [Int: AnimationCacheItemAccessor.FrameInfo] = [:]
|
||||||
|
for i in 0 ..< Int(numFrames) {
|
||||||
|
guard dataLength >= offset + 4 + 4 + 4 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let frameStart = readUInt32(data: data, offset: offset)
|
||||||
|
offset += 4
|
||||||
|
let frameLength = readUInt32(data: data, offset: offset)
|
||||||
|
offset += 4
|
||||||
|
let frameDuration = readUInt32(data: data, offset: offset)
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
frameMapping[i] = AnimationCacheItemAccessor.FrameInfo(range: Int(frameStart) ..< Int(frameStart + frameLength), duration: Double(frameDuration))
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemAccessor = AnimationCacheItemAccessor(data: data, frameMapping: frameMapping, format: .rgba(width: Int(width), height: Int(height), bytesPerRow: Int(bytesPerRow)))
|
||||||
|
|
||||||
|
return AnimationCacheItem(numFrames: Int(numFrames), getFrame: { index in
|
||||||
|
return itemAccessor.getFrame(index: index)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class AnimationCacheImpl: AnimationCache {
|
||||||
|
private final class Impl {
|
||||||
|
private final class ItemContext {
|
||||||
|
let subscribers = Bag<(AnimationCacheItem?) -> Void>()
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let queue: Queue
|
||||||
|
private let basePath: String
|
||||||
|
private let allocateTempFile: () -> String
|
||||||
|
|
||||||
|
private var itemContexts: [String: ItemContext] = [:]
|
||||||
|
|
||||||
|
init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String) {
|
||||||
|
self.queue = queue
|
||||||
|
self.basePath = basePath
|
||||||
|
self.allocateTempFile = allocateTempFile
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
|
||||||
|
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId))
|
||||||
|
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
|
||||||
|
let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)"
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: itemPath) {
|
||||||
|
completion(loadItem(path: itemPath))
|
||||||
|
|
||||||
|
return EmptyDisposable
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemContext: ItemContext
|
||||||
|
var beginFetch = false
|
||||||
|
if let current = self.itemContexts[sourceId] {
|
||||||
|
itemContext = current
|
||||||
|
} else {
|
||||||
|
itemContext = ItemContext()
|
||||||
|
self.itemContexts[sourceId] = itemContext
|
||||||
|
beginFetch = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let queue = self.queue
|
||||||
|
let index = itemContext.subscribers.add(completion)
|
||||||
|
|
||||||
|
if beginFetch {
|
||||||
|
let tempPath = self.allocateTempFile()
|
||||||
|
guard let writer = AnimationCacheItemWriterImpl(tempPath: tempPath, completion: { [weak self, weak itemContext] success in
|
||||||
|
queue.async {
|
||||||
|
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.itemContexts.removeValue(forKey: sourceId)
|
||||||
|
|
||||||
|
guard success else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let _ = try? FileManager.default.moveItem(atPath: tempPath, toPath: itemPath) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let item = loadItem(path: itemPath) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for f in itemContext.subscribers.copyItems() {
|
||||||
|
f(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) else {
|
||||||
|
return EmptyDisposable
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchDisposable = fetch(writer)
|
||||||
|
|
||||||
|
itemContext.disposable.set(ActionDisposable {
|
||||||
|
fetchDisposable.dispose()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActionDisposable { [weak self, weak itemContext] in
|
||||||
|
queue.async {
|
||||||
|
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemContext.subscribers.remove(index)
|
||||||
|
if itemContext.subscribers.isEmpty {
|
||||||
|
itemContext.disposable.dispose()
|
||||||
|
strongSelf.itemContexts.removeValue(forKey: sourceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let queue: Queue
|
||||||
|
private let impl: QueueLocalObject<Impl>
|
||||||
|
|
||||||
|
public init(basePath: String, allocateTempFile: @escaping () -> String) {
|
||||||
|
let queue = Queue()
|
||||||
|
self.queue = queue
|
||||||
|
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||||
|
return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal<AnimationCacheItem?, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
|
||||||
|
self.impl.with { impl in
|
||||||
|
disposable.set(impl.get(sourceId: sourceId, fetch: fetch, completion: { result in
|
||||||
|
subscriber.putNext(result)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
|> runOn(self.queue)
|
||||||
|
}
|
||||||
|
}
|
18
submodules/TelegramUI/Components/EmojiKeyboard/BUILD
Normal file
18
submodules/TelegramUI/Components/EmojiKeyboard/BUILD
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "EmojiKeyboard",
|
||||||
|
module_name = "EmojiKeyboard",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,4 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
|
22
submodules/TelegramUI/Components/LottieAnimationCache/BUILD
Normal file
22
submodules/TelegramUI/Components/LottieAnimationCache/BUILD
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "LottieAnimationCache",
|
||||||
|
module_name = "LottieAnimationCache",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
|
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/rlottie:RLottieBinding",
|
||||||
|
"//submodules/GZip:GZip",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,23 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AnimationCache
|
||||||
|
import Display
|
||||||
|
import RLottieBinding
|
||||||
|
import GZip
|
||||||
|
|
||||||
|
public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: AnimationCacheItemWriter) {
|
||||||
|
let decompressedData = TGGUnzipData(data, 512 * 1024) ?? data
|
||||||
|
guard let animation = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
|
||||||
|
writer.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let size = CGSize(width: width, height: height)
|
||||||
|
let context = DrawingContext(size: size, scale: 1.0, opaque: false, clear: true)
|
||||||
|
let frameDuration = 1.0 / Double(animation.frameRate)
|
||||||
|
for i in 0 ..< animation.frameCount {
|
||||||
|
animation.renderFrame(with: i, into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(context.scaledSize.width), height: Int32(context.scaledSize.height), bytesPerRow: Int32(context.bytesPerRow))
|
||||||
|
writer.add(bytes: context.bytes, length: context.length, width: Int(context.scaledSize.width), height: Int(context.scaledSize.height), bytesPerRow: Int(context.bytesPerRow), duration: frameDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.finish()
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "MultiAnimationRenderer",
|
||||||
|
module_name = "MultiAnimationRenderer",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,421 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Display
|
||||||
|
import AnimationCache
|
||||||
|
|
||||||
|
public protocol MultiAnimationRenderer: AnyObject {
|
||||||
|
func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable
|
||||||
|
}
|
||||||
|
|
||||||
|
open class MultiAnimationRenderTarget: SimpleLayer {
|
||||||
|
fileprivate let deinitCallbacks = Bag<() -> Void>()
|
||||||
|
fileprivate let updateStateCallbacks = Bag<() -> Void>()
|
||||||
|
|
||||||
|
public final var shouldBeAnimating: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if self.shouldBeAnimating != oldValue {
|
||||||
|
for f in self.updateStateCallbacks.copyItems() {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
for f in self.deinitCallbacks.copyItems() {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convertFrameToImage(frame: AnimationCacheItemFrame) -> UIImage? {
|
||||||
|
switch frame.format {
|
||||||
|
case let .rgba(width, height, bytesPerRow):
|
||||||
|
let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow)
|
||||||
|
let range = frame.range
|
||||||
|
frame.data.withUnsafeBytes { bytes -> Void in
|
||||||
|
memcpy(context.bytes, bytes.baseAddress!.advanced(by: range.lowerBound), min(context.length, range.upperBound - range.lowerBound))
|
||||||
|
}
|
||||||
|
return context.generateImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class FrameGroup {
|
||||||
|
let image: UIImage
|
||||||
|
let size: CGSize
|
||||||
|
let frameRange: Range<Int>
|
||||||
|
let count: Int
|
||||||
|
let skip: Int
|
||||||
|
|
||||||
|
init?(item: AnimationCacheItem, baseFrameIndex: Int, count: Int, skip: Int) {
|
||||||
|
if count == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(count % skip == 0)
|
||||||
|
|
||||||
|
let actualCount = count / skip
|
||||||
|
|
||||||
|
guard let firstFrame = item.getFrame(index: baseFrameIndex % item.numFrames) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch firstFrame.format {
|
||||||
|
case let .rgba(width, height, bytesPerRow):
|
||||||
|
let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height * actualCount)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow)
|
||||||
|
for i in stride(from: baseFrameIndex, to: baseFrameIndex + count, by: skip) {
|
||||||
|
let frame: AnimationCacheItemFrame
|
||||||
|
if i == baseFrameIndex {
|
||||||
|
frame = firstFrame
|
||||||
|
} else {
|
||||||
|
if let nextFrame = item.getFrame(index: i % item.numFrames) {
|
||||||
|
frame = nextFrame
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let localFrameIndex = (i - baseFrameIndex) / skip
|
||||||
|
|
||||||
|
frame.data.withUnsafeBytes { bytes -> Void in
|
||||||
|
memcpy(context.bytes.advanced(by: localFrameIndex * height * bytesPerRow), bytes.baseAddress!.advanced(by: frame.range.lowerBound), height * bytesPerRow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let image = context.generateImage() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.image = image
|
||||||
|
self.size = CGSize(width: CGFloat(width), height: CGFloat(height))
|
||||||
|
self.frameRange = baseFrameIndex ..< (baseFrameIndex + count)
|
||||||
|
self.count = count
|
||||||
|
self.skip = skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contentsRect(index: Int) -> CGRect? {
|
||||||
|
if !self.frameRange.contains(index) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let actualCount = self.count / self.skip
|
||||||
|
let localIndex = (index - self.frameRange.lowerBound) / self.skip
|
||||||
|
|
||||||
|
let itemHeight = 1.0 / CGFloat(actualCount)
|
||||||
|
return CGRect(origin: CGPoint(x: 0.0, y: CGFloat(localIndex) * itemHeight), size: CGSize(width: 1.0, height: itemHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class LoadFrameGroupTask {
|
||||||
|
let task: () -> () -> Void
|
||||||
|
|
||||||
|
init(task: @escaping () -> () -> Void) {
|
||||||
|
self.task = task
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ItemAnimationContext {
|
||||||
|
static let queue = Queue(name: "ItemAnimationContext", qos: .default)
|
||||||
|
|
||||||
|
private let cache: AnimationCache
|
||||||
|
private let stateUpdated: () -> Void
|
||||||
|
|
||||||
|
private var disposable: Disposable?
|
||||||
|
private var displayLink: ConstantDisplayLinkAnimator?
|
||||||
|
private var frameIndex: Int = 0
|
||||||
|
private var item: AnimationCacheItem?
|
||||||
|
|
||||||
|
private var currentFrameGroup: FrameGroup?
|
||||||
|
private var isLoadingFrameGroup: Bool = false
|
||||||
|
|
||||||
|
private(set) var isPlaying: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if self.isPlaying != oldValue {
|
||||||
|
self.stateUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let targets = Bag<Weak<MultiAnimationRenderTarget>>()
|
||||||
|
|
||||||
|
init(cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) {
|
||||||
|
self.cache = cache
|
||||||
|
self.stateUpdated = stateUpdated
|
||||||
|
|
||||||
|
self.disposable = cache.get(sourceId: itemId, fetch: fetch).start(next: { [weak self] item in
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
guard let strongSelf = self, let item = item else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.item = item
|
||||||
|
strongSelf.updateIsPlaying()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable?.dispose()
|
||||||
|
self.displayLink?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateIsPlaying() {
|
||||||
|
var isPlaying = true
|
||||||
|
if self.item == nil {
|
||||||
|
isPlaying = false
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldBeAnimating = false
|
||||||
|
for target in self.targets.copyItems() {
|
||||||
|
if let target = target.value {
|
||||||
|
if target.shouldBeAnimating {
|
||||||
|
shouldBeAnimating = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !shouldBeAnimating {
|
||||||
|
isPlaying = false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isPlaying = isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
func animationTick() -> LoadFrameGroupTask? {
|
||||||
|
return self.update(advanceFrame: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(advanceFrame: Bool) -> LoadFrameGroupTask? {
|
||||||
|
guard let item = self.item else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentFrame = self.frameIndex % item.numFrames
|
||||||
|
|
||||||
|
if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.frameRange.contains(currentFrame) {
|
||||||
|
} else if !self.isLoadingFrameGroup {
|
||||||
|
self.currentFrameGroup = nil
|
||||||
|
self.isLoadingFrameGroup = true
|
||||||
|
|
||||||
|
return LoadFrameGroupTask(task: { [weak self] in
|
||||||
|
let possibleCounts: [Int] = [10, 12, 14, 16, 18, 20]
|
||||||
|
let countIndex = Int.random(in: 0 ..< possibleCounts.count)
|
||||||
|
let currentFrameGroup = FrameGroup(item: item, baseFrameIndex: currentFrame, count: possibleCounts[countIndex], skip: 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.isLoadingFrameGroup = false
|
||||||
|
|
||||||
|
if let currentFrameGroup = currentFrameGroup {
|
||||||
|
strongSelf.currentFrameGroup = currentFrameGroup
|
||||||
|
for target in strongSelf.targets.copyItems() {
|
||||||
|
target.value?.contents = currentFrameGroup.image.cgImage
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = strongSelf.update(advanceFrame: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if advanceFrame {
|
||||||
|
self.frameIndex += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if let currentFrameGroup = self.currentFrameGroup, let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) {
|
||||||
|
for target in self.targets.copyItems() {
|
||||||
|
target.value?.contentsRect = contentsRect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||||
|
private final class GroupContext {
|
||||||
|
private let stateUpdated: () -> Void
|
||||||
|
|
||||||
|
private var itemContexts: [String: ItemAnimationContext] = [:]
|
||||||
|
|
||||||
|
private(set) var isPlaying: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if self.isPlaying != oldValue {
|
||||||
|
self.stateUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(stateUpdated: @escaping () -> Void) {
|
||||||
|
self.stateUpdated = stateUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable {
|
||||||
|
let itemContext: ItemAnimationContext
|
||||||
|
if let current = self.itemContexts[itemId] {
|
||||||
|
itemContext = current
|
||||||
|
} else {
|
||||||
|
itemContext = ItemAnimationContext(cache: cache, itemId: itemId, fetch: fetch, stateUpdated: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.updateIsPlaying()
|
||||||
|
})
|
||||||
|
self.itemContexts[itemId] = itemContext
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = itemContext.targets.add(Weak(target))
|
||||||
|
|
||||||
|
let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemContext.targets.remove(index)
|
||||||
|
if itemContext.targets.isEmpty {
|
||||||
|
strongSelf.itemContexts.removeValue(forKey: itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in
|
||||||
|
guard let itemContext = itemContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemContext.updateIsPlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActionDisposable { [weak self, weak itemContext, weak target] in
|
||||||
|
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let target = target {
|
||||||
|
target.deinitCallbacks.remove(deinitIndex)
|
||||||
|
target.updateStateCallbacks.remove(updateStateIndex)
|
||||||
|
}
|
||||||
|
itemContext.targets.remove(index)
|
||||||
|
if itemContext.targets.isEmpty {
|
||||||
|
strongSelf.itemContexts.removeValue(forKey: itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateIsPlaying() {
|
||||||
|
var isPlaying = false
|
||||||
|
for (_, itemContext) in self.itemContexts {
|
||||||
|
if itemContext.isPlaying {
|
||||||
|
isPlaying = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isPlaying = isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
func animationTick() -> [LoadFrameGroupTask] {
|
||||||
|
var tasks: [LoadFrameGroupTask] = []
|
||||||
|
for (_, itemContext) in self.itemContexts {
|
||||||
|
if itemContext.isPlaying {
|
||||||
|
if let task = itemContext.animationTick() {
|
||||||
|
tasks.append(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var groupContexts: [String: GroupContext] = [:]
|
||||||
|
private var displayLink: ConstantDisplayLinkAnimator?
|
||||||
|
|
||||||
|
private(set) var isPlaying: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if self.isPlaying != oldValue {
|
||||||
|
if self.isPlaying {
|
||||||
|
if self.displayLink == nil {
|
||||||
|
self.displayLink = ConstantDisplayLinkAnimator { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.animationTick()
|
||||||
|
}
|
||||||
|
self.displayLink?.frameInterval = 2
|
||||||
|
self.displayLink?.isPaused = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let displayLink = self.displayLink {
|
||||||
|
self.displayLink = nil
|
||||||
|
displayLink.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable {
|
||||||
|
let groupContext: GroupContext
|
||||||
|
if let current = self.groupContexts[groupId] {
|
||||||
|
groupContext = current
|
||||||
|
} else {
|
||||||
|
groupContext = GroupContext(stateUpdated: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.updateIsPlaying()
|
||||||
|
})
|
||||||
|
self.groupContexts[groupId] = groupContext
|
||||||
|
}
|
||||||
|
|
||||||
|
let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, fetch: fetch)
|
||||||
|
|
||||||
|
return ActionDisposable {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateIsPlaying() {
|
||||||
|
var isPlaying = false
|
||||||
|
for (_, groupContext) in self.groupContexts {
|
||||||
|
if groupContext.isPlaying {
|
||||||
|
isPlaying = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isPlaying = isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animationTick() {
|
||||||
|
var tasks: [LoadFrameGroupTask] = []
|
||||||
|
for (_, groupContext) in self.groupContexts {
|
||||||
|
if groupContext.isPlaying {
|
||||||
|
tasks.append(contentsOf: groupContext.animationTick())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tasks.isEmpty {
|
||||||
|
ItemAnimationContext.queue.async {
|
||||||
|
var completions: [() -> Void] = []
|
||||||
|
for task in tasks {
|
||||||
|
let complete = task.task()
|
||||||
|
completions.append(complete)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !completions.isEmpty {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
for completion in completions {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2903,7 +2903,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
if !forceOpen {
|
if !forceOpen {
|
||||||
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||||
if !found, let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.message.id == message.id, let (action, _, _, _, _) = itemNode.playMediaWithSound() {
|
if !found, let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.message.id == message.id, let (action, _, _, _, _) = itemNode.playMediaWithSound() {
|
||||||
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
|
if case let .visible(fraction, _) = itemNode.visibility, fraction > 0.7 {
|
||||||
action(Double(timestamp))
|
action(Double(timestamp))
|
||||||
} else {
|
} else {
|
||||||
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
|
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
|
||||||
@ -3564,7 +3564,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}, cancelInteractiveKeyboardGestures: { [weak self] in
|
}, cancelInteractiveKeyboardGestures: { [weak self] in
|
||||||
(self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
|
(self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
|
||||||
self?.chatDisplayNode.cancelInteractiveKeyboardGestures()
|
self?.chatDisplayNode.cancelInteractiveKeyboardGestures()
|
||||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(backgroundNode: self.chatBackgroundNode))
|
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode))
|
||||||
|
|
||||||
self.controllerInteraction = controllerInteraction
|
self.controllerInteraction = controllerInteraction
|
||||||
|
|
||||||
@ -8827,7 +8827,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
var hasUnconsumed = false
|
var hasUnconsumed = false
|
||||||
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||||
if let itemNode = itemNode as? ChatMessageItemView, let (action, _, _, isUnconsumed, _) = itemNode.playMediaWithSound() {
|
if let itemNode = itemNode as? ChatMessageItemView, let (action, _, _, isUnconsumed, _) = itemNode.playMediaWithSound() {
|
||||||
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
|
if case let .visible(fraction, _) = itemNode.visibility, fraction > 0.7 {
|
||||||
actions.insert((isUnconsumed, action), at: 0)
|
actions.insert((isUnconsumed, action), at: 0)
|
||||||
if !hasUnconsumed && isUnconsumed {
|
if !hasUnconsumed && isUnconsumed {
|
||||||
hasUnconsumed = true
|
hasUnconsumed = true
|
||||||
|
@ -332,63 +332,4 @@ public final class ChatControllerInteraction {
|
|||||||
|
|
||||||
self.presentationContext = presentationContext
|
self.presentationContext = presentationContext
|
||||||
}
|
}
|
||||||
|
|
||||||
static var `default`: ChatControllerInteraction {
|
|
||||||
return ChatControllerInteraction(openMessage: { _, _ in
|
|
||||||
return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in
|
|
||||||
}, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ 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 }, presentControllerInCurrent: { _, _ in }, navigationController: {
|
|
||||||
return nil
|
|
||||||
}, chatControllerNode: {
|
|
||||||
return nil
|
|
||||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
|
||||||
}, canSetupReply: { _ in
|
|
||||||
return .none
|
|
||||||
}, navigateToFirstDateMessage: { _, _ in
|
|
||||||
}, requestRedeliveryOfFailedMessages: { _ in
|
|
||||||
}, addContact: { _ in
|
|
||||||
}, rateCall: { _, _, _ in
|
|
||||||
}, requestSelectMessagePollOptions: { _, _ in
|
|
||||||
}, requestOpenMessagePollResults: { _, _ in
|
|
||||||
}, openAppStorePage: {
|
|
||||||
}, displayMessageTooltip: { _, _, _, _ in
|
|
||||||
}, seekToTimecode: { _, _, _ in
|
|
||||||
}, scheduleCurrentMessage: {
|
|
||||||
}, sendScheduledMessagesNow: { _ in
|
|
||||||
}, editScheduledMessagesTime: { _ in
|
|
||||||
}, performTextSelectionAction: { _, _, _ in
|
|
||||||
}, displayImportedMessageTooltip: { _ in
|
|
||||||
}, displaySwipeToReplyHint: {
|
|
||||||
}, dismissReplyMarkupMessage: { _ in
|
|
||||||
}, openMessagePollResults: { _, _ in
|
|
||||||
}, openPollCreation: { _ in
|
|
||||||
}, displayPollSolution: { _, _ in
|
|
||||||
}, displayPsa: { _, _ in
|
|
||||||
}, displayDiceTooltip: { _ in
|
|
||||||
}, animateDiceSuccess: { _ in
|
|
||||||
}, displayPremiumStickerTooltip: { _, _ in
|
|
||||||
}, openPeerContextMenu: { _, _, _, _, _ in
|
|
||||||
}, openMessageReplies: { _, _, _ in
|
|
||||||
}, openReplyThreadOriginalMessage: { _ in
|
|
||||||
}, openMessageStats: { _ in
|
|
||||||
}, editMessageMedia: { _, _ in
|
|
||||||
}, copyText: { _ in
|
|
||||||
}, displayUndo: { _ in
|
|
||||||
}, isAnimatingMessage: { _ in
|
|
||||||
return false
|
|
||||||
}, getMessageTransitionNode: {
|
|
||||||
return nil
|
|
||||||
}, updateChoosingSticker: { _ in
|
|
||||||
}, commitEmojiInteraction: { _, _, _, _ in
|
|
||||||
}, openLargeEmojiInfo: { _, _, _ in
|
|
||||||
}, openJoinLink: { _ in
|
|
||||||
}, openWebView: { _, _, _, _ in
|
|
||||||
}, requestMessageUpdate: { _ in
|
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
|
||||||
pollActionState: ChatInterfacePollActionState(),
|
|
||||||
stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false),
|
|
||||||
presentationContext: ChatPresentationContext(backgroundNode: nil)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -514,7 +514,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||||
|
|
||||||
self.textInputPanelNode = ChatTextInputPanelNode(presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(backgroundNode: backgroundNode), presentController: { [weak self] controller in
|
self.textInputPanelNode = ChatTextInputPanelNode(presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in
|
||||||
self?.interfaceInteraction?.presentController(controller, nil)
|
self?.interfaceInteraction?.presentController(controller, nil)
|
||||||
})
|
})
|
||||||
self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage
|
self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage
|
||||||
@ -630,7 +630,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
if let itemNode = itemNode as? ChatMessageItemView, let (_, soundEnabled, isVideoMessage, _, badgeNode) = itemNode.playMediaWithSound(), let node = badgeNode {
|
if let itemNode = itemNode as? ChatMessageItemView, let (_, soundEnabled, isVideoMessage, _, badgeNode) = itemNode.playMediaWithSound(), let node = badgeNode {
|
||||||
if soundEnabled {
|
if soundEnabled {
|
||||||
skip = true
|
skip = true
|
||||||
} else if !skip && !isVideoMessage, case let .visible(fraction) = itemNode.visibility {
|
} else if !skip && !isVideoMessage, case let .visible(fraction, _) = itemNode.visibility {
|
||||||
nodes.insert((fraction, itemNode, node), at: 0)
|
nodes.insert((fraction, itemNode, node), at: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -692,50 +692,58 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasRateTranscription = false
|
||||||
if let audioTranscription = audioTranscription {
|
if let audioTranscription = audioTranscription {
|
||||||
|
hasRateTranscription = true
|
||||||
actions.insert(.custom(ChatRateTranscriptionContextItem(context: context, message: message, action: { [weak context] value in
|
actions.insert(.custom(ChatRateTranscriptionContextItem(context: context, message: message, action: { [weak context] value in
|
||||||
guard let context = context else {
|
guard let context = context else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = context.engine.messages.rateAudioTranscription(messageId: message.id, id: audioTranscription.id, isGood: value).start()
|
let _ = context.engine.messages.rateAudioTranscription(messageId: message.id, id: audioTranscription.id, isGood: value).start()
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
let content: UndoOverlayContent = .info(title: nil, text: "Thank you for your feedback.")
|
||||||
|
controllerInteraction.displayUndo(content)
|
||||||
}), false), at: 0)
|
}), false), at: 0)
|
||||||
actions.insert(.separator, at: 1)
|
actions.insert(.separator, at: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
for media in message.media {
|
if !hasRateTranscription {
|
||||||
if let file = media as? TelegramMediaFile, let size = file.size, size < 1 * 1024 * 1024, let duration = file.duration, duration < 60, (["audio/mpeg", "audio/mp3", "audio/mpeg3", "audio/ogg"] as [String]).contains(file.mimeType.lowercased()) {
|
for media in message.media {
|
||||||
let fileName = file.fileName ?? "Tone"
|
if let file = media as? TelegramMediaFile, let size = file.size, size < 1 * 1024 * 1024, let duration = file.duration, duration < 60, (["audio/mpeg", "audio/mp3", "audio/mpeg3", "audio/ogg"] as [String]).contains(file.mimeType.lowercased()) {
|
||||||
|
let fileName = file.fileName ?? "Tone"
|
||||||
var isAlreadyAdded = false
|
|
||||||
if let notificationSoundList = notificationSoundList, notificationSoundList.sounds.contains(where: { $0.file.fileId == file.fileId }) {
|
var isAlreadyAdded = false
|
||||||
isAlreadyAdded = true
|
if let notificationSoundList = notificationSoundList, notificationSoundList.sounds.contains(where: { $0.file.fileId == file.fileId }) {
|
||||||
}
|
isAlreadyAdded = true
|
||||||
|
}
|
||||||
if !isAlreadyAdded {
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
if !isAlreadyAdded {
|
||||||
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_SaveForNotifications, icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}, action: { _, f in
|
|
||||||
f(.default)
|
|
||||||
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_SaveForNotifications, icon: { theme in
|
||||||
let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 }))
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.actionSheet.primaryTextColor)
|
||||||
if size > settings.maxSize {
|
}, action: { _, f in
|
||||||
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string))
|
f(.default)
|
||||||
} else if Double(duration) > Double(settings.maxDuration) {
|
|
||||||
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string))
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
} else {
|
|
||||||
let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file))
|
let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 }))
|
||||||
|> deliverOnMainQueue).start(completed: {
|
if size > settings.maxSize {
|
||||||
controllerInteraction.displayUndo(.notificationSoundAdded(title: presentationData.strings.Notifications_UploadSuccess_Title, text: presentationData.strings.Notifications_SaveSuccess_Text, action: {
|
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string))
|
||||||
controllerInteraction.navigationController()?.pushViewController(notificationsAndSoundsController(context: context, exceptionsList: nil))
|
} else if Double(duration) > Double(settings.maxDuration) {
|
||||||
}))
|
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string))
|
||||||
})
|
} else {
|
||||||
}
|
let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file))
|
||||||
})))
|
|> deliverOnMainQueue).start(completed: {
|
||||||
actions.append(.separator)
|
controllerInteraction.displayUndo(.notificationSoundAdded(title: presentationData.strings.Notifications_UploadSuccess_Title, text: presentationData.strings.Notifications_SaveSuccess_Text, action: {
|
||||||
|
controllerInteraction.navigationController()?.pushViewController(notificationsAndSoundsController(context: context, exceptionsList: nil))
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
actions.append(.separator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2371,6 +2379,7 @@ private final class ChatRateTranscriptionContextItemNode: ASDisplayNode, Context
|
|||||||
self.textNode.isAccessibilityElement = false
|
self.textNode.isAccessibilityElement = false
|
||||||
self.textNode.isUserInteractionEnabled = false
|
self.textNode.isUserInteractionEnabled = false
|
||||||
self.textNode.displaysAsynchronously = false
|
self.textNode.displaysAsynchronously = false
|
||||||
|
//TODO:localizable
|
||||||
self.textNode.attributedText = NSAttributedString(string: "Rate Transcription", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
self.textNode.attributedText = NSAttributedString(string: "Rate Transcription", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
||||||
self.textNode.maximumNumberOfLines = 1
|
self.textNode.maximumNumberOfLines = 1
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ import Markdown
|
|||||||
import WallpaperBackgroundNode
|
import WallpaperBackgroundNode
|
||||||
import ChatPresentationInterfaceState
|
import ChatPresentationInterfaceState
|
||||||
import ChatMessageBackground
|
import ChatMessageBackground
|
||||||
|
import AnimationCache
|
||||||
|
import MultiAnimationRenderer
|
||||||
|
|
||||||
enum InternalBubbleTapAction {
|
enum InternalBubbleTapAction {
|
||||||
case action(() -> Void)
|
case action(() -> Void)
|
||||||
@ -288,9 +290,37 @@ private enum ContentNodeOperation {
|
|||||||
|
|
||||||
class ChatPresentationContext {
|
class ChatPresentationContext {
|
||||||
weak var backgroundNode: WallpaperBackgroundNode?
|
weak var backgroundNode: WallpaperBackgroundNode?
|
||||||
|
let animationCache: AnimationCache
|
||||||
|
let animationRenderer: MultiAnimationRenderer
|
||||||
|
|
||||||
init(backgroundNode: WallpaperBackgroundNode?) {
|
init(context: AccountContext, backgroundNode: WallpaperBackgroundNode?) {
|
||||||
self.backgroundNode = backgroundNode
|
self.backgroundNode = backgroundNode
|
||||||
|
|
||||||
|
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||||||
|
return TempBox.shared.tempFile(fileName: "file").path
|
||||||
|
})
|
||||||
|
self.animationRenderer = MultiAnimationRendererImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mapVisibility(_ visibility: ListViewItemNodeVisibility, boundsSize: CGSize, insets: UIEdgeInsets, to contentNode: ChatMessageBubbleContentNode) -> ListViewItemNodeVisibility {
|
||||||
|
switch visibility {
|
||||||
|
case .none:
|
||||||
|
return .none
|
||||||
|
case let .visible(fraction, subRect):
|
||||||
|
var subRect = subRect
|
||||||
|
subRect.origin.x = 0.0
|
||||||
|
subRect.size.width = 10000.0
|
||||||
|
|
||||||
|
subRect.origin.y = boundsSize.height - insets.top - (subRect.origin.y + subRect.height)
|
||||||
|
|
||||||
|
let contentNodeFrame = contentNode.frame
|
||||||
|
if contentNodeFrame.intersects(subRect) {
|
||||||
|
let intersectionRect = contentNodeFrame.intersection(subRect)
|
||||||
|
return .visible(fraction, intersectionRect.offsetBy(dx: 0.0, dy: -contentNodeFrame.minY))
|
||||||
|
} else {
|
||||||
|
return .visible(fraction, CGRect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -489,12 +519,22 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
|
|
||||||
private var currentSwipeAction: ChatControllerInteractionSwipeAction?
|
private var currentSwipeAction: ChatControllerInteractionSwipeAction?
|
||||||
|
|
||||||
|
//private let debugNode: ASDisplayNode
|
||||||
|
|
||||||
override var visibility: ListViewItemNodeVisibility {
|
override var visibility: ListViewItemNodeVisibility {
|
||||||
didSet {
|
didSet {
|
||||||
if self.visibility != oldValue {
|
if self.visibility != oldValue {
|
||||||
for contentNode in self.contentNodes {
|
for contentNode in self.contentNodes {
|
||||||
contentNode.visibility = self.visibility
|
contentNode.visibility = mapVisibility(self.visibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*switch self.visibility {
|
||||||
|
case let .visible(_, subRect):
|
||||||
|
let topEdge = self.bounds.height - self.insets.top - (subRect.origin.y + subRect.height)
|
||||||
|
self.debugNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topEdge), size: CGSize(width: 100.0, height: 2.0))
|
||||||
|
case .none:
|
||||||
|
break
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -513,8 +553,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
|
|
||||||
self.messageAccessibilityArea = AccessibilityAreaNode()
|
self.messageAccessibilityArea = AccessibilityAreaNode()
|
||||||
|
|
||||||
|
//self.debugNode = ASDisplayNode()
|
||||||
|
//self.debugNode.backgroundColor = .blue
|
||||||
|
|
||||||
super.init(layerBacked: false)
|
super.init(layerBacked: false)
|
||||||
|
|
||||||
|
//self.addSubnode(self.debugNode)
|
||||||
|
|
||||||
self.mainContainerNode.shouldBegin = { [weak self] location in
|
self.mainContainerNode.shouldBegin = { [weak self] location in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return false
|
return false
|
||||||
@ -2650,7 +2695,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
}
|
}
|
||||||
containerSupernode.addSubnode(contentNode)
|
containerSupernode.addSubnode(contentNode)
|
||||||
|
|
||||||
contentNode.visibility = strongSelf.visibility
|
|
||||||
contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in
|
contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in
|
||||||
contextSourceNode?.updateDistractionFreeMode?(value)
|
contextSourceNode?.updateDistractionFreeMode?(value)
|
||||||
}
|
}
|
||||||
@ -2729,6 +2773,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
} else {
|
} else {
|
||||||
contentNode.frame = contentNodeFrame
|
contentNode.frame = contentNodeFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contentNode.visibility = mapVisibility(strongSelf.visibility, boundsSize: layout.contentSize, insets: strongSelf.insets, to: contentNode)
|
||||||
|
|
||||||
contentNodeIndex += 1
|
contentNodeIndex += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,9 @@ import TelegramAnimatedStickerNode
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import YuvConversion
|
import YuvConversion
|
||||||
|
import AnimationCache
|
||||||
|
import LottieAnimationCache
|
||||||
|
import MultiAnimationRenderer
|
||||||
|
|
||||||
private final class CachedChatMessageText {
|
private final class CachedChatMessageText {
|
||||||
let text: String
|
let text: String
|
||||||
@ -61,7 +64,7 @@ private final class InlineStickerItem: Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class InlineStickerItemLayer: SimpleLayer {
|
private final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||||
static let queue = Queue()
|
static let queue = Queue()
|
||||||
|
|
||||||
struct Key: Hashable {
|
struct Key: Hashable {
|
||||||
@ -70,26 +73,50 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let file: TelegramMediaFile
|
private let file: TelegramMediaFile
|
||||||
private let source: AnimatedStickerNodeSource
|
//private var frameSource: QueueLocalObject<AnimatedStickerDirectFrameSource>?
|
||||||
private var frameSource: QueueLocalObject<AnimatedStickerDirectFrameSource>?
|
|
||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
private var fetchDisposable: Disposable?
|
private var fetchDisposable: Disposable?
|
||||||
|
|
||||||
private var isInHierarchyValue: Bool = false
|
private var isInHierarchyValue: Bool = false
|
||||||
var isVisibleForAnimations: Bool = false {
|
var isVisibleForAnimations: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
self.updatePlayback()
|
if self.isVisibleForAnimations != oldValue {
|
||||||
|
self.updatePlayback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var displayLink: ConstantDisplayLinkAnimator?
|
private var displayLink: ConstantDisplayLinkAnimator?
|
||||||
|
|
||||||
init(context: AccountContext, file: TelegramMediaFile) {
|
init(context: AccountContext, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer) {
|
||||||
self.source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
|
|
||||||
self.file = file
|
self.file = file
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
self.disposable = renderer.add(groupId: "inlineEmoji", target: self, cache: cache, itemId: file.resource.id.stringRepresentation, fetch: { writer in
|
||||||
|
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
|
||||||
|
|
||||||
|
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
|
||||||
|
guard let result = result else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else {
|
||||||
|
writer.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let scale = min(2.0, UIScreenScale)
|
||||||
|
cacheLottieAnimation(data: data, width: Int(24 * scale), height: Int(24 * scale), writer: writer)
|
||||||
|
})
|
||||||
|
|
||||||
|
let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()
|
||||||
|
|
||||||
|
return ActionDisposable {
|
||||||
|
dataDisposable.dispose()
|
||||||
|
fetchDisposable.dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||||
|
|
||||||
self.disposable = (self.source.directDataPath(attemptSynchronously: false)
|
self.disposable = (self.source.directDataPath(attemptSynchronously: false)
|
||||||
|> filter { $0 != nil }
|
|> filter { $0 != nil }
|
||||||
@ -109,17 +136,11 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
self.fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()
|
self.fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()*/
|
||||||
}
|
}
|
||||||
|
|
||||||
override init(layer: Any) {
|
override init(layer: Any) {
|
||||||
guard let layer = layer as? InlineStickerItemLayer else {
|
preconditionFailure()
|
||||||
preconditionFailure()
|
|
||||||
}
|
|
||||||
self.source = layer.source
|
|
||||||
self.file = layer.file
|
|
||||||
|
|
||||||
super.init(layer: layer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init?(coder: NSCoder) {
|
required public init?(coder: NSCoder) {
|
||||||
@ -142,8 +163,11 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updatePlayback() {
|
private func updatePlayback() {
|
||||||
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations && self.frameSource != nil
|
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
|
||||||
if shouldBePlaying != (self.displayLink != nil) {
|
|
||||||
|
self.shouldBeAnimating = shouldBePlaying
|
||||||
|
|
||||||
|
/*if shouldBePlaying != (self.displayLink != nil) {
|
||||||
if shouldBePlaying {
|
if shouldBePlaying {
|
||||||
self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
|
self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||||
self?.loadNextFrame()
|
self?.loadNextFrame()
|
||||||
@ -153,12 +177,10 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
|||||||
self.displayLink?.invalidate()
|
self.displayLink?.invalidate()
|
||||||
self.displayLink = nil
|
self.displayLink = nil
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private var didRequestFrame = false
|
/*private func loadNextFrame() {
|
||||||
|
|
||||||
private func loadNextFrame() {
|
|
||||||
guard let frameSource = self.frameSource else {
|
guard let frameSource = self.frameSource else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -200,7 +222,7 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||||
@ -224,11 +246,25 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
override var visibility: ListViewItemNodeVisibility {
|
override var visibility: ListViewItemNodeVisibility {
|
||||||
didSet {
|
didSet {
|
||||||
let wasVisible = oldValue != .none
|
if !self.inlineStickerItemLayers.isEmpty {
|
||||||
let isVisible = self.visibility != .none
|
if oldValue != self.visibility {
|
||||||
if wasVisible != isVisible {
|
for (_, itemLayer) in self.inlineStickerItemLayers {
|
||||||
for (_, itemLayer) in self.inlineStickerItemLayers {
|
let isItemVisible: Bool
|
||||||
itemLayer.isVisibleForAnimations = isVisible
|
switch self.visibility {
|
||||||
|
case .none:
|
||||||
|
isItemVisible = false
|
||||||
|
case let .visible(_, subRect):
|
||||||
|
var subRect = subRect
|
||||||
|
subRect.origin.x = 0.0
|
||||||
|
subRect.size.width = 10000.0
|
||||||
|
if itemLayer.frame.intersects(subRect) {
|
||||||
|
isItemVisible = true
|
||||||
|
} else {
|
||||||
|
isItemVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemLayer.isVisibleForAnimations = isItemVisible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -491,7 +527,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor
|
updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor
|
||||||
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile)
|
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile)
|
||||||
|
|
||||||
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}\u{00a0}]", attributes: updatedAttributes)
|
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}]", attributes: updatedAttributes)
|
||||||
//updatedString.insert(insertString, at: NSRange(substringRange, in: updatedString.string).upperBound)
|
//updatedString.insert(insertString, at: NSRange(substringRange, in: updatedString.string).upperBound)
|
||||||
updatedString.replaceCharacters(in: range, with: insertString)
|
updatedString.replaceCharacters(in: range, with: insertString)
|
||||||
}
|
}
|
||||||
@ -687,7 +723,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
strongSelf.textAccessibilityOverlayNode.frame = textFrame
|
strongSelf.textAccessibilityOverlayNode.frame = textFrame
|
||||||
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
|
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
|
||||||
|
|
||||||
strongSelf.updateInlineStickers(context: item.context, textLayout: textLayout)
|
strongSelf.updateInlineStickers(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, textLayout: textLayout)
|
||||||
|
|
||||||
if let statusSizeAndApply = statusSizeAndApply {
|
if let statusSizeAndApply = statusSizeAndApply {
|
||||||
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0), completion: nil)
|
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0), completion: nil)
|
||||||
@ -718,7 +754,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateInlineStickers(context: AccountContext, textLayout: TextNodeLayout?) {
|
private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?) {
|
||||||
var nextIndexById: [MediaId: Int] = [:]
|
var nextIndexById: [MediaId: Int] = [:]
|
||||||
var validIds: [InlineStickerItemLayer.Key] = []
|
var validIds: [InlineStickerItemLayer.Key] = []
|
||||||
|
|
||||||
@ -739,7 +775,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
if let current = self.inlineStickerItemLayers[id] {
|
if let current = self.inlineStickerItemLayers[id] {
|
||||||
itemLayer = current
|
itemLayer = current
|
||||||
} else {
|
} else {
|
||||||
itemLayer = InlineStickerItemLayer(context: context, file: stickerItem.file)
|
itemLayer = InlineStickerItemLayer(context: context, file: stickerItem.file, cache: cache, renderer: renderer)
|
||||||
self.inlineStickerItemLayers[id] = itemLayer
|
self.inlineStickerItemLayers[id] = itemLayer
|
||||||
self.textNode.layer.addSublayer(itemLayer)
|
self.textNode.layer.addSublayer(itemLayer)
|
||||||
itemLayer.isVisibleForAnimations = self.isVisibleForAnimations
|
itemLayer.isVisibleForAnimations = self.isVisibleForAnimations
|
||||||
|
@ -208,10 +208,10 @@ private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated:
|
|||||||
}
|
}
|
||||||
if let resultNode = resultNode {
|
if let resultNode = resultNode {
|
||||||
var nodeToEnsure = resultNode
|
var nodeToEnsure = resultNode
|
||||||
if case let .visible(resultVisibility) = resultNode.visibility, resultVisibility == 1.0 {
|
if case let .visible(resultVisibility, _) = resultNode.visibility, resultVisibility == 1.0 {
|
||||||
if let previousNode = previousNode, case let .visible(previousVisibility) = previousNode.visibility, previousVisibility < 0.5 {
|
if let previousNode = previousNode, case let .visible(previousVisibility, _) = previousNode.visibility, previousVisibility < 0.5 {
|
||||||
nodeToEnsure = previousNode
|
nodeToEnsure = previousNode
|
||||||
} else if let nextNode = nextNode, case let .visible(nextVisibility) = nextNode.visibility, nextVisibility < 0.5 {
|
} else if let nextNode = nextNode, case let .visible(nextVisibility, _) = nextNode.visibility, nextVisibility < 0.5 {
|
||||||
nodeToEnsure = nextNode
|
nodeToEnsure = nextNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -538,7 +538,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: self.backgroundNode))
|
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode))
|
||||||
self.controllerInteraction = controllerInteraction
|
self.controllerInteraction = controllerInteraction
|
||||||
|
|
||||||
self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in
|
self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in
|
||||||
|
@ -189,10 +189,10 @@ private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated:
|
|||||||
}
|
}
|
||||||
if let resultNode = resultNode {
|
if let resultNode = resultNode {
|
||||||
var nodeToEnsure = resultNode
|
var nodeToEnsure = resultNode
|
||||||
if case let .visible(resultVisibility) = resultNode.visibility, resultVisibility == 1.0 {
|
if case let .visible(resultVisibility, _) = resultNode.visibility, resultVisibility == 1.0 {
|
||||||
if let previousNode = previousNode, case let .visible(previousVisibility) = previousNode.visibility, previousVisibility < 0.5 {
|
if let previousNode = previousNode, case let .visible(previousVisibility, _) = previousNode.visibility, previousVisibility < 0.5 {
|
||||||
nodeToEnsure = previousNode
|
nodeToEnsure = previousNode
|
||||||
} else if let nextNode = nextNode, case let .visible(nextVisibility) = nextNode.visibility, nextVisibility < 0.5 {
|
} else if let nextNode = nextNode, case let .visible(nextVisibility, _) = nextNode.visibility, nextVisibility < 0.5 {
|
||||||
nodeToEnsure = nextNode
|
nodeToEnsure = nextNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
|||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: true), presentationContext: ChatPresentationContext(backgroundNode: nil))
|
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: true), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||||
|
|
||||||
self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||||||
}, openWebView: { _, _, _, _ in
|
}, openWebView: { _, _, _, _ in
|
||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil))
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||||
|
|
||||||
self.dimNode = ASDisplayNode()
|
self.dimNode = ASDisplayNode()
|
||||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||||
|
@ -2320,7 +2320,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil))
|
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||||
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
|
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
|
@ -1333,7 +1333,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
}, requestMessageUpdate: { _ in
|
}, requestMessageUpdate: { _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: backgroundNode as? WallpaperBackgroundNode))
|
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))
|
||||||
|
|
||||||
var entryAttributes = ChatMessageEntryAttributes()
|
var entryAttributes = ChatMessageEntryAttributes()
|
||||||
entryAttributes.isCentered = isCentered
|
entryAttributes.isCentered = isCentered
|
||||||
@ -1462,8 +1462,6 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let defaultChatControllerInteraction = ChatControllerInteraction.default
|
|
||||||
|
|
||||||
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {
|
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {
|
||||||
if let _ = peer as? TelegramGroup {
|
if let _ = peer as? TelegramGroup {
|
||||||
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, callMessages: [])
|
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, callMessages: [])
|
||||||
|
@ -159,7 +159,10 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
|
|||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
if count >= maxAnimatedEmojisInText {
|
if count >= maxAnimatedEmojisInText {
|
||||||
|
#if DEBUG
|
||||||
|
#else
|
||||||
stop = true
|
stop = true
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user