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{
|
||||
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) {
|
||||
CHECK_CANCELLED_AND_RETURN_NIL(ASGraphicsEndImageContext());
|
||||
if (isCancelledBlock()) return;
|
||||
block();
|
||||
}
|
||||
|
||||
UIImage *image = ASGraphicsGetImageAndEndCurrentContext();
|
||||
});
|
||||
|
||||
ASDN_DELAY_FOR_DISPLAY();
|
||||
return image;
|
||||
@ -228,17 +226,13 @@ using AS::MutexLocker;
|
||||
displayBlock = ^id{
|
||||
CHECK_CANCELLED_AND_RETURN_NIL();
|
||||
|
||||
if (shouldCreateGraphicsContext) {
|
||||
ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
|
||||
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
|
||||
}
|
||||
|
||||
__block UIImage *image = nil;
|
||||
void (^workWithContext)() = ^{
|
||||
CGContextRef currentContext = UIGraphicsGetCurrentContext();
|
||||
UIImage *image = nil;
|
||||
|
||||
if (shouldCreateGraphicsContext && !currentContext) {
|
||||
//ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
|
||||
return nil;
|
||||
ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
|
||||
return;
|
||||
}
|
||||
|
||||
// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
|
||||
@ -252,14 +246,15 @@ using AS::MutexLocker;
|
||||
}
|
||||
|
||||
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
|
||||
ASDN_DELAY_FOR_DISPLAY();
|
||||
};
|
||||
|
||||
if (shouldCreateGraphicsContext) {
|
||||
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
|
||||
image = ASGraphicsGetImageAndEndCurrentContext();
|
||||
}
|
||||
|
||||
ASDN_DELAY_FOR_DISPLAY();
|
||||
return ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, workWithContext);
|
||||
} else {
|
||||
workWithContext();
|
||||
return image;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -309,9 +304,6 @@ using AS::MutexLocker;
|
||||
}
|
||||
|
||||
__instanceLock__.lock();
|
||||
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
|
||||
CGFloat cornerRadius = _cornerRadius;
|
||||
CGFloat contentsScale = _contentsScaleForDisplay;
|
||||
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
|
||||
__instanceLock__.unlock();
|
||||
|
||||
@ -320,48 +312,6 @@ using AS::MutexLocker;
|
||||
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
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
@ -1634,7 +1599,6 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
|
||||
}
|
||||
else if (newRoundingType == ASCornerRoundingTypeClipping) {
|
||||
// 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/ASCGImageBuffer.h>
|
||||
#import <AsyncDisplayKit/ASAssert.h>
|
||||
#import <AsyncDisplayKit/ASConfigurationInternal.h>
|
||||
#import <AsyncDisplayKit/ASInternalHelpers.h>
|
||||
#import <UIKit/UIGraphics.h>
|
||||
#import <UIKit/UIImage.h>
|
||||
#import <objc/runtime.h>
|
||||
#import <AsyncDisplayKit/ASAvailability.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
|
||||
#define ASPerformBlockWithTraitCollection(work, traitCollection) \
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) { \
|
||||
UITraitCollection *uiTraitCollection = ASPrimitiveTraitCollectionToUITraitCollection(traitCollection); \
|
||||
[uiTraitCollection performAsCurrentTraitCollection:^{ \
|
||||
work(); \
|
||||
}];\
|
||||
} else { \
|
||||
work(); \
|
||||
}
|
||||
#else
|
||||
#define ASPerformBlockWithTraitCollection(work, traitCollection) work();
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A key used to associate CGContextRef -> NSMutableData, nonatomic retain.
|
||||
*
|
||||
* That way the data will be released when the context dies. If they pull an image,
|
||||
* we will retain the data object (in a CGDataProvider) before releasing the context.
|
||||
*/
|
||||
static UInt8 __contextDataAssociationKey;
|
||||
|
||||
#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)) {
|
||||
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
|
||||
return;
|
||||
if (AS_AVAILABLE_IOS_TVOS(12, 12)) {
|
||||
// nop. We always use automatic range on iOS >= 12.
|
||||
} 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();
|
||||
UIImage *ASGraphicsCreateImageWithOptions(CGSize size, BOOL opaque, CGFloat scale, UIImage *sourceImage,
|
||||
asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled,
|
||||
void (^NS_NOESCAPE work)())
|
||||
{
|
||||
return ASGraphicsCreateImage(ASPrimitiveTraitCollectionMakeDefault(), size, opaque, scale, sourceImage, isCancelled, work);
|
||||
}
|
||||
|
||||
// Make transparent ref context.
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1);
|
||||
refCtxTransparent = CGContextRetain(UIGraphicsGetCurrentContext());
|
||||
UIGraphicsEndImageContext();
|
||||
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 (true /*ASActivateExperimentalFeature(ASExperimentalDrawingGlobal)*/) {
|
||||
// If they used default scale, reuse one of two preferred formats.
|
||||
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);
|
||||
});
|
||||
|
||||
// These options are taken from UIGraphicsBeginImageContext.
|
||||
CGContextRef refCtx = opaque ? refCtxOpaque : refCtxTransparent;
|
||||
CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(refCtx);
|
||||
|
||||
if (scale == 0) {
|
||||
scale = ASScreenScale();
|
||||
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];
|
||||
}
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
|
||||
UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext() NS_RETURNS_RETAINED
|
||||
{
|
||||
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
|
||||
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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()
|
||||
{
|
||||
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
|
||||
UIGraphicsEndImageContext();
|
||||
return;
|
||||
}
|
||||
|
||||
UIGraphicsPopContext();
|
||||
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
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <AsyncDisplayKit/ASBaseDefines.h>
|
||||
#import <CoreGraphics/CoreGraphics.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.
|
||||
*/
|
||||
#import <AsyncDisplayKit/ASBlockTypes.h>
|
||||
#import <AsyncDisplayKit/ASTraitCollection.h>
|
||||
|
||||
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.
|
||||
* @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.
|
||||
*/
|
||||
AS_EXTERN UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext(void) NS_RETURNS_RETAINED;
|
||||
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.
|
||||
* @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.
|
||||
*/
|
||||
AS_EXTERN void ASGraphicsEndImageContext(void);
|
||||
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
|
||||
|
@ -18,7 +18,9 @@ public func authorizationCurrentOptionText(_ type: SentAuthorizationCodeType, st
|
||||
let body = MarkdownAttributeSet(font: Font.regular(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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -35,7 +37,13 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType,
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
return (NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
||||
} else {
|
||||
|
@ -606,7 +606,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
let wasVisible = self.visibilityStatus
|
||||
let isVisible: Bool
|
||||
switch self.visibility {
|
||||
case let .visible(fraction):
|
||||
case let .visible(fraction, _):
|
||||
isVisible = fraction > 0.2
|
||||
case .none:
|
||||
isVisible = false
|
||||
|
@ -492,7 +492,7 @@ public struct DeviceGraphicsContextSettings {
|
||||
public class DrawingContext {
|
||||
public let size: CGSize
|
||||
public let scale: CGFloat
|
||||
private let scaledSize: CGSize
|
||||
public let scaledSize: CGSize
|
||||
public let bytesPerRow: Int
|
||||
private let bitmapInfo: CGBitmapInfo
|
||||
public let length: Int
|
||||
@ -525,7 +525,7 @@ public class DrawingContext {
|
||||
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)
|
||||
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.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale)
|
||||
|
||||
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width))
|
||||
self.length = bytesPerRow * Int(scaledSize.height)
|
||||
self.bytesPerRow = bytesPerRow ?? DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width))
|
||||
self.length = self.bytesPerRow * Int(scaledSize.height)
|
||||
|
||||
self.imageBuffer = ASCGImageBuffer(length: UInt(self.length))
|
||||
|
||||
|
@ -3685,7 +3685,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
let itemContentFrame = itemNode.apparentContentFrame
|
||||
let intersection = itemContentFrame.intersection(visibilityRect)
|
||||
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
|
||||
if !onlyPositive {
|
||||
|
@ -49,24 +49,7 @@ public struct ListViewItemNodeLayout {
|
||||
|
||||
public enum ListViewItemNodeVisibility: Equatable {
|
||||
case none
|
||||
case visible(CGFloat)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
case visible(CGFloat, CGRect)
|
||||
}
|
||||
|
||||
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() {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
@ -116,3 +124,39 @@ public final class ManagedFile {
|
||||
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/EditableChatTextNode:EditableChatTextNode",
|
||||
"//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/LocalAudioTranscription:LocalAudioTranscription",
|
||||
] + 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 {
|
||||
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
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))
|
||||
} else {
|
||||
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
|
||||
@ -3564,7 +3564,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}, cancelInteractiveKeyboardGestures: { [weak self] in
|
||||
(self?.view.window as? WindowHost)?.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
|
||||
|
||||
@ -8827,7 +8827,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var hasUnconsumed = false
|
||||
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
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)
|
||||
if !hasUnconsumed && isUnconsumed {
|
||||
hasUnconsumed = true
|
||||
|
@ -332,63 +332,4 @@ public final class ChatControllerInteraction {
|
||||
|
||||
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.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.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 soundEnabled {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -692,17 +692,24 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
}
|
||||
}
|
||||
|
||||
var hasRateTranscription = false
|
||||
if let audioTranscription = audioTranscription {
|
||||
hasRateTranscription = true
|
||||
actions.insert(.custom(ChatRateTranscriptionContextItem(context: context, message: message, action: { [weak context] value in
|
||||
guard let context = context else {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
actions.insert(.separator, at: 1)
|
||||
}
|
||||
|
||||
if !hasRateTranscription {
|
||||
for media in message.media {
|
||||
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"
|
||||
@ -739,6 +746,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isReplyThreadHead = false
|
||||
if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation {
|
||||
@ -2371,6 +2379,7 @@ private final class ChatRateTranscriptionContextItemNode: ASDisplayNode, Context
|
||||
self.textNode.isAccessibilityElement = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.displaysAsynchronously = false
|
||||
//TODO:localizable
|
||||
self.textNode.attributedText = NSAttributedString(string: "Rate Transcription", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
|
||||
|
@ -24,6 +24,8 @@ import Markdown
|
||||
import WallpaperBackgroundNode
|
||||
import ChatPresentationInterfaceState
|
||||
import ChatMessageBackground
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
|
||||
enum InternalBubbleTapAction {
|
||||
case action(() -> Void)
|
||||
@ -288,9 +290,37 @@ private enum ContentNodeOperation {
|
||||
|
||||
class ChatPresentationContext {
|
||||
weak var backgroundNode: WallpaperBackgroundNode?
|
||||
let animationCache: AnimationCache
|
||||
let animationRenderer: MultiAnimationRenderer
|
||||
|
||||
init(backgroundNode: WallpaperBackgroundNode?) {
|
||||
init(context: AccountContext, backgroundNode: WallpaperBackgroundNode?) {
|
||||
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 let debugNode: ASDisplayNode
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
if self.visibility != oldValue {
|
||||
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.debugNode = ASDisplayNode()
|
||||
//self.debugNode.backgroundColor = .blue
|
||||
|
||||
super.init(layerBacked: false)
|
||||
|
||||
//self.addSubnode(self.debugNode)
|
||||
|
||||
self.mainContainerNode.shouldBegin = { [weak self] location in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
@ -2650,7 +2695,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
containerSupernode.addSubnode(contentNode)
|
||||
|
||||
contentNode.visibility = strongSelf.visibility
|
||||
contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in
|
||||
contextSourceNode?.updateDistractionFreeMode?(value)
|
||||
}
|
||||
@ -2729,6 +2773,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
} else {
|
||||
contentNode.frame = contentNodeFrame
|
||||
}
|
||||
|
||||
contentNode.visibility = mapVisibility(strongSelf.visibility, boundsSize: layout.contentSize, insets: strongSelf.insets, to: contentNode)
|
||||
|
||||
contentNodeIndex += 1
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,9 @@ import TelegramAnimatedStickerNode
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import YuvConversion
|
||||
import AnimationCache
|
||||
import LottieAnimationCache
|
||||
import MultiAnimationRenderer
|
||||
|
||||
private final class CachedChatMessageText {
|
||||
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()
|
||||
|
||||
struct Key: Hashable {
|
||||
@ -70,26 +73,50 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
||||
}
|
||||
|
||||
private let file: TelegramMediaFile
|
||||
private let source: AnimatedStickerNodeSource
|
||||
private var frameSource: QueueLocalObject<AnimatedStickerDirectFrameSource>?
|
||||
//private var frameSource: QueueLocalObject<AnimatedStickerDirectFrameSource>?
|
||||
private var disposable: Disposable?
|
||||
private var fetchDisposable: Disposable?
|
||||
|
||||
private var isInHierarchyValue: Bool = false
|
||||
var isVisibleForAnimations: Bool = false {
|
||||
didSet {
|
||||
if self.isVisibleForAnimations != oldValue {
|
||||
self.updatePlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
private var displayLink: ConstantDisplayLinkAnimator?
|
||||
|
||||
init(context: AccountContext, file: TelegramMediaFile) {
|
||||
self.source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
|
||||
init(context: AccountContext, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer) {
|
||||
self.file = file
|
||||
|
||||
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)
|
||||
|> filter { $0 != nil }
|
||||
@ -109,18 +136,12 @@ 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) {
|
||||
guard let layer = layer as? InlineStickerItemLayer else {
|
||||
preconditionFailure()
|
||||
}
|
||||
self.source = layer.source
|
||||
self.file = layer.file
|
||||
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
@ -142,8 +163,11 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
||||
}
|
||||
|
||||
private func updatePlayback() {
|
||||
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations && self.frameSource != nil
|
||||
if shouldBePlaying != (self.displayLink != nil) {
|
||||
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
|
||||
|
||||
self.shouldBeAnimating = shouldBePlaying
|
||||
|
||||
/*if shouldBePlaying != (self.displayLink != nil) {
|
||||
if shouldBePlaying {
|
||||
self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||
self?.loadNextFrame()
|
||||
@ -153,12 +177,10 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
||||
self.displayLink?.invalidate()
|
||||
self.displayLink = nil
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
private var didRequestFrame = false
|
||||
|
||||
private func loadNextFrame() {
|
||||
/*private func loadNextFrame() {
|
||||
guard let frameSource = self.frameSource else {
|
||||
return
|
||||
}
|
||||
@ -200,7 +222,7 @@ private final class InlineStickerItemLayer: SimpleLayer {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
@ -224,11 +246,25 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
let wasVisible = oldValue != .none
|
||||
let isVisible = self.visibility != .none
|
||||
if wasVisible != isVisible {
|
||||
if !self.inlineStickerItemLayers.isEmpty {
|
||||
if oldValue != self.visibility {
|
||||
for (_, itemLayer) in self.inlineStickerItemLayers {
|
||||
itemLayer.isVisibleForAnimations = isVisible
|
||||
let isItemVisible: Bool
|
||||
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("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.replaceCharacters(in: range, with: insertString)
|
||||
}
|
||||
@ -687,7 +723,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.textAccessibilityOverlayNode.frame = textFrame
|
||||
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 {
|
||||
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 validIds: [InlineStickerItemLayer.Key] = []
|
||||
|
||||
@ -739,7 +775,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if let current = self.inlineStickerItemLayers[id] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
itemLayer = InlineStickerItemLayer(context: context, file: stickerItem.file)
|
||||
itemLayer = InlineStickerItemLayer(context: context, file: stickerItem.file, cache: cache, renderer: renderer)
|
||||
self.inlineStickerItemLayers[id] = itemLayer
|
||||
self.textNode.layer.addSublayer(itemLayer)
|
||||
itemLayer.isVisibleForAnimations = self.isVisibleForAnimations
|
||||
|
@ -208,10 +208,10 @@ private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated:
|
||||
}
|
||||
if let resultNode = resultNode {
|
||||
var nodeToEnsure = resultNode
|
||||
if case let .visible(resultVisibility) = resultNode.visibility, resultVisibility == 1.0 {
|
||||
if let previousNode = previousNode, case let .visible(previousVisibility) = previousNode.visibility, previousVisibility < 0.5 {
|
||||
if case let .visible(resultVisibility, _) = resultNode.visibility, resultVisibility == 1.0 {
|
||||
if let previousNode = previousNode, case let .visible(previousVisibility, _) = previousNode.visibility, previousVisibility < 0.5 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -538,7 +538,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, 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.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in
|
||||
|
@ -189,10 +189,10 @@ private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated:
|
||||
}
|
||||
if let resultNode = resultNode {
|
||||
var nodeToEnsure = resultNode
|
||||
if case let .visible(resultVisibility) = resultNode.visibility, resultVisibility == 1.0 {
|
||||
if let previousNode = previousNode, case let .visible(previousVisibility) = previousNode.visibility, previousVisibility < 0.5 {
|
||||
if case let .visible(resultVisibility, _) = resultNode.visibility, resultVisibility == 1.0 {
|
||||
if let previousNode = previousNode, case let .visible(previousVisibility, _) = previousNode.visibility, previousVisibility < 0.5 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, 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))
|
||||
|
||||
|
@ -155,7 +155,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, 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.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
@ -2320,7 +2320,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, 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
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
@ -1333,7 +1333,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, 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()
|
||||
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? {
|
||||
if let _ = peer as? TelegramGroup {
|
||||
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
|
||||
if count >= maxAnimatedEmojisInText {
|
||||
#if DEBUG
|
||||
#else
|
||||
stop = true
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user