diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+AsyncDisplay.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+AsyncDisplay.mm index 068f5509a7..b59edb7395 100644 --- a/submodules/AsyncDisplayKit/Source/ASDisplayNode+AsyncDisplay.mm +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+AsyncDisplay.mm @@ -212,14 +212,12 @@ using AS::MutexLocker; displayBlock = ^id{ CHECK_CANCELLED_AND_RETURN_NIL(); - ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); - - for (dispatch_block_t block in displayBlocks) { - CHECK_CANCELLED_AND_RETURN_NIL(ASGraphicsEndImageContext()); - block(); - } - - UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); + UIImage *image = ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, ^{ + for (dispatch_block_t block in displayBlocks) { + if (isCancelledBlock()) return; + block(); + } + }); ASDN_DELAY_FOR_DISPLAY(); return image; @@ -228,38 +226,35 @@ using AS::MutexLocker; displayBlock = ^id{ CHECK_CANCELLED_AND_RETURN_NIL(); - if (shouldCreateGraphicsContext) { - ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); - CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); ); - } - - CGContextRef currentContext = UIGraphicsGetCurrentContext(); - UIImage *image = nil; - - if (shouldCreateGraphicsContext && !currentContext) { - //ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size)); - return nil; - } - - // For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or - // _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs. - [self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters]; - - if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly. - image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock]; - } else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext. - [self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; - } - - [self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor]; - - if (shouldCreateGraphicsContext) { - CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); ); - image = ASGraphicsGetImageAndEndCurrentContext(); - } - - ASDN_DELAY_FOR_DISPLAY(); - return image; + __block UIImage *image = nil; + void (^workWithContext)() = ^{ + CGContextRef currentContext = UIGraphicsGetCurrentContext(); + + if (shouldCreateGraphicsContext && !currentContext) { + 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 + // _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs. + [self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters]; + + if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly. + image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock]; + } else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext. + [self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; + } + + [self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor]; + ASDN_DELAY_FOR_DISPLAY(); + }; + + if (shouldCreateGraphicsContext) { + 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 diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm index 29fe8db76b..ee57c04c8e 100644 --- a/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm @@ -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]; } } } diff --git a/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm b/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm index b950613d0d..ab16cfc47d 100644 --- a/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm +++ b/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm @@ -7,161 +7,130 @@ // #import -#import #import #import #import -#import -#import -#import +#import -/** - * 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; -} -/** - * 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; +#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 -#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(); - - // Make transparent ref context. - UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1); - refCtxTransparent = CGContextRetain(UIGraphicsGetCurrentContext()); - UIGraphicsEndImageContext(); - }); - - // These options are taken from UIGraphicsBeginImageContext. - CGContextRef refCtx = opaque ? refCtxOpaque : refCtxTransparent; - CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(refCtx); - - if (scale == 0) { - scale = ASScreenScale(); - } - size_t intWidth = (size_t)ceil(size.width * scale); - size_t intHeight = (size_t)ceil(size.height * scale); - size_t bitsPerComponent = CGBitmapContextGetBitsPerComponent(refCtx); - size_t bytesPerRow = CGBitmapContextGetBitsPerPixel(refCtx) * intWidth / 8; - bytesPerRow = ASGraphicsGetAlignedBytesPerRow(bytesPerRow); - size_t bufferSize = bytesPerRow * intHeight; - CGColorSpaceRef colorspace = CGBitmapContextGetColorSpace(refCtx); - - // We create our own buffer, and wrap the context around that. This way we can prevent - // the copy that usually gets made when you form a CGImage from the context. - ASCGImageBuffer *buffer = [[ASCGImageBuffer alloc] initWithLength:bufferSize]; - - CGContextRef context = CGBitmapContextCreate(buffer.mutableBytes, intWidth, intHeight, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo); - - // Transfer ownership of the data to the context. So that if the context - // is destroyed before we create an image from it, the data will be released. - objc_setAssociatedObject((__bridge id)context, &__contextDataAssociationKey, buffer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - - // Set the CTM to account for iOS orientation & specified scale. - // If only we could use CGContextSetBaseCTM. It doesn't - // seem like there are any consequences for our use case - // but we'll be on the look out. The internet hinted that it - // affects shadowing but I tested and shadowing works. - CGContextTranslateCTM(context, 0, intHeight); - CGContextScaleCTM(context, scale, -scale); - - // Save the state so we can restore it and recover our scale in GetImageAndEnd - CGContextSaveGState(context); - - // Transfer context ownership to the UIKit stack. - UIGraphicsPushContext(context); - CGContextRelease(context); } -UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext() NS_RETURNS_RETAINED +UIImage *ASGraphicsCreateImageWithOptions(CGSize size, BOOL opaque, CGFloat scale, UIImage *sourceImage, + asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled, + void (^NS_NOESCAPE work)()) { - if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return image; - } - - // Pop the context and make sure we have one. - CGContextRef context = UIGraphicsGetCurrentContext(); - if (context == NULL) { - ASDisplayNodeCFailAssert(@"Can't end image context without having begun one."); - return nil; - } - - // Read the device-specific ICC-based color space to use for the image. - // For DeviceRGB contexts (e.g. UIGraphics), CGBitmapContextCreateImage - // generates an image in a device-specific color space (for wide color support). - // We replicate that behavior, even though at this time CA does not - // require the image to be in this space. Plain DeviceRGB images seem - // to be treated exactly the same, but better safe than sorry. - static CGColorSpaceRef imageColorSpace; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0); - UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext(); - imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage)); - ASDisplayNodeCAssertNotNil(imageColorSpace, nil); - UIGraphicsEndImageContext(); - }); - - // Retrieve our buffer and create a CGDataProvider from it. - ASCGImageBuffer *buffer = objc_getAssociatedObject((__bridge id)context, &__contextDataAssociationKey); - ASDisplayNodeCAssertNotNil(buffer, nil); - CGDataProviderRef provider = [buffer createDataProviderAndInvalidate]; - - // Create the CGImage. Options taken from CGBitmapContextCreateImage. - CGImageRef cgImg = CGImageCreate(CGBitmapContextGetWidth(context), CGBitmapContextGetHeight(context), CGBitmapContextGetBitsPerComponent(context), CGBitmapContextGetBitsPerPixel(context), CGBitmapContextGetBytesPerRow(context), imageColorSpace, CGBitmapContextGetBitmapInfo(context), provider, NULL, true, kCGRenderingIntentDefault); - CGDataProviderRelease(provider); - - // We saved our GState right after setting the CTM so that we could restore it - // here and get the original scale back. - CGContextRestoreGState(context); - CGFloat scale = CGContextGetCTM(context).a; - - // Note: popping from the UIKit stack will probably destroy the context. - context = NULL; - UIGraphicsPopContext(); - - UIImage *result = [[UIImage alloc] initWithCGImage:cgImg scale:scale orientation:UIImageOrientationUp]; - CGImageRelease(cgImg); - return result; + return ASGraphicsCreateImage(ASPrimitiveTraitCollectionMakeDefault(), size, opaque, scale, sourceImage, isCancelled, work); } -void ASGraphicsEndImageContext() -{ - if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { - UIGraphicsEndImageContext(); - return; +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); + }); + + UIGraphicsImageRendererFormat *format; + if (sourceImage) { + if (sourceImage.renderingMode == UIImageRenderingModeAlwaysTemplate) { + // Template images will be black and transparent, so if we use + // sourceImage.imageRenderFormat it will assume a grayscale color space. + // This is not good because a template image should be able to tint to any color, + // so we'll just use the default here. + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + format = [UIGraphicsImageRendererFormat preferredFormat]; + } else { + format = [UIGraphicsImageRendererFormat defaultFormat]; + } + } else { + format = sourceImage.imageRendererFormat; + } + // We only want the private bits (color space and bits per component) from the image. + // We have our own ideas about opacity and scale. + format.opaque = opaque; + format.scale = scale; + } else if (scale == 0 || scale == ASScreenScale()) { + format = opaque ? opaqueFormat : defaultFormat; + } else { + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + format = [UIGraphicsImageRendererFormat preferredFormat]; + } else { + format = [UIGraphicsImageRendererFormat defaultFormat]; + } + if (opaque) format.opaque = YES; + format.scale = scale; + ASConfigureExtendedRange(format); + } + + // Avoid using the imageWithActions: method because it does not support cancellation at the + // last moment i.e. before actually creating the resulting image. + __block UIImage *image; + NSError *error; + [[[UIGraphicsImageRenderer alloc] initWithSize:size format:format] + runDrawingActions:^(UIGraphicsImageRendererContext *rendererContext) { + ASDisplayNodeCAssert(UIGraphicsGetCurrentContext(), @"Should have a context!"); + ASPerformBlockWithTraitCollection(work, traitCollection); + } + completionActions:^(UIGraphicsImageRendererContext *rendererContext) { + if (isCancelled == nil || !isCancelled()) { + image = rendererContext.currentImage; + } + } + error:&error]; + if (error) { + NSCAssert(NO, @"Error drawing: %@", error); + } + return image; + } } - - UIGraphicsPopContext(); + + // Bad OS or experiment flag. Use UIGraphics* API. + UIGraphicsBeginImageContextWithOptions(size, opaque, scale); + ASPerformBlockWithTraitCollection(work, traitCollection) + UIImage *image = nil; + if (isCancelled == nil || !isCancelled()) { + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + return image; +} + +UIImage *ASGraphicsCreateImageWithTraitCollectionAndOptions(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * sourceImage, void (NS_NOESCAPE ^work)()) { + return ASGraphicsCreateImage(traitCollection, size, opaque, scale, sourceImage, nil, work); } diff --git a/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASGraphicsContext.h b/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASGraphicsContext.h index 1ef56518c9..4a93ca3957 100644 --- a/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASGraphicsContext.h +++ b/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASGraphicsContext.h @@ -6,46 +6,60 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import #import #import -#import - -@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 +#import 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. - * - * Behavior is the same as UIGraphicsGetImageFromCurrentImageContext followed by UIGraphicsEndImageContext. - */ -AS_EXTERN UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext(void) NS_RETURNS_RETAINED; +* 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. +* +* @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. +*/ +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. - * - * Behavior is the same as UIGraphicsEndImageContext. - */ -AS_EXTERN void ASGraphicsEndImageContext(void); +* A wrapper for the UIKit drawing APIs. +* +* @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. +*/ +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 diff --git a/submodules/AuthorizationUI/Sources/AuthorizationOptionText.swift b/submodules/AuthorizationUI/Sources/AuthorizationOptionText.swift index da9faf84d3..fe3b3fa0a3 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationOptionText.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationOptionText.swift @@ -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 { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 0794f311b0..d3e9ac1b55 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -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 diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index 4b05dc557f..9ea4f9cda7 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -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)) diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 66a4f920fd..040c153dd1 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -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 { diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index 0abfb04fd9..e8a53198da 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -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 { diff --git a/submodules/ManagedFile/Sources/ManagedFile.swift b/submodules/ManagedFile/Sources/ManagedFile.swift index 0203f3692a..d43ab46369 100644 --- a/submodules/ManagedFile/Sources/ManagedFile.swift +++ b/submodules/ManagedFile/Sources/ManagedFile.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index a7bee6a82b..82699b6145 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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({ diff --git a/submodules/TelegramUI/Components/AnimationCache/BUILD b/submodules/TelegramUI/Components/AnimationCache/BUILD new file mode 100644 index 0000000000..c5773d4849 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift new file mode 100644 index 0000000000..77d0498976 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -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 + public let format: Format + public let duration: Double + + public init(data: Data, range: Range, 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 +} + +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 + 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 + + 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 { + 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) + } +} diff --git a/submodules/TelegramUI/Components/EmojiKeyboard/BUILD b/submodules/TelegramUI/Components/EmojiKeyboard/BUILD new file mode 100644 index 0000000000..525cbd2996 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiKeyboard/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/EmojiKeyboard/Sources/EmojiKeyboard.swift b/submodules/TelegramUI/Components/EmojiKeyboard/Sources/EmojiKeyboard.swift new file mode 100644 index 0000000000..2cc37ab552 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiKeyboard/Sources/EmojiKeyboard.swift @@ -0,0 +1,4 @@ +import Foundation +import UIKit +import Display + diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/BUILD b/submodules/TelegramUI/Components/LottieAnimationCache/BUILD new file mode 100644 index 0000000000..434120005d --- /dev/null +++ b/submodules/TelegramUI/Components/LottieAnimationCache/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift new file mode 100644 index 0000000000..1049ce1d21 --- /dev/null +++ b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift @@ -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() +} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD new file mode 100644 index 0000000000..e5a853e167 --- /dev/null +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift new file mode 100644 index 0000000000..e7b404962e --- /dev/null +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -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 + 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>() + + 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() + } + } + } + } + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 0b21abf7ec..5d5868cfdc 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 2802633036..e2f8f16e7a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -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) - ) - } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index af990eda74..96c4c3bffe 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -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) } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index c75739d6b6..c7993d5b9e 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -692,50 +692,58 @@ 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) } - 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" - - var isAlreadyAdded = false - if let notificationSoundList = notificationSoundList, notificationSoundList.sounds.contains(where: { $0.file.fileId == file.fileId }) { - isAlreadyAdded = true - } - - if !isAlreadyAdded { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_SaveForNotifications, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.default) - + 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" + + var isAlreadyAdded = false + if let notificationSoundList = notificationSoundList, notificationSoundList.sounds.contains(where: { $0.file.fileId == file.fileId }) { + isAlreadyAdded = true + } + + if !isAlreadyAdded { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 })) - if size > settings.maxSize { - controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)) - } else if Double(duration) > Double(settings.maxDuration) { - controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string)) - } else { - let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file)) - |> deliverOnMainQueue).start(completed: { - controllerInteraction.displayUndo(.notificationSoundAdded(title: presentationData.strings.Notifications_UploadSuccess_Title, text: presentationData.strings.Notifications_SaveSuccess_Text, action: { - controllerInteraction.navigationController()?.pushViewController(notificationsAndSoundsController(context: context, exceptionsList: nil)) - })) - }) - } - }))) - actions.append(.separator) + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_SaveForNotifications, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 })) + if size > settings.maxSize { + controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)) + } else if Double(duration) > Double(settings.maxDuration) { + controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string)) + } else { + let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file)) + |> deliverOnMainQueue).start(completed: { + controllerInteraction.displayUndo(.notificationSoundAdded(title: presentationData.strings.Notifications_UploadSuccess_Title, text: presentationData.strings.Notifications_SaveSuccess_Text, action: { + controllerInteraction.navigationController()?.pushViewController(notificationsAndSoundsController(context: context, exceptionsList: nil)) + })) + }) + } + }))) + actions.append(.separator) + } } } } @@ -2371,6 +2379,7 @@ private final class ChatRateTranscriptionContextItemNode: ASDisplayNode, Context self.textNode.isAccessibilityElement = false self.textNode.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 diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 2e484ec885..8cd4ae2e57 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 7fd81efd49..c2a30e2735 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -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? + //private var frameSource: QueueLocalObject? private var disposable: Disposable? private var fetchDisposable: Disposable? private var isInHierarchyValue: Bool = false var isVisibleForAnimations: Bool = false { didSet { - self.updatePlayback() + 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,17 +136,11 @@ private final class InlineStickerItemLayer: SimpleLayer { } }) - self.fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() + self.fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()*/ } override init(layer: Any) { - guard let layer = layer as? InlineStickerItemLayer else { - preconditionFailure() - } - self.source = layer.source - self.file = layer.file - - super.init(layer: layer) + preconditionFailure() } required public init?(coder: NSCoder) { @@ -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 { - for (_, itemLayer) in self.inlineStickerItemLayers { - itemLayer.isVisibleForAnimations = isVisible + if !self.inlineStickerItemLayers.isEmpty { + if oldValue != self.visibility { + for (_, itemLayer) in self.inlineStickerItemLayers { + 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 diff --git a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift index 8a5bb06b7f..f1008f5460 100644 --- a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift @@ -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 } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 72b84b5d0c..fa72a7a018 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index e24a60d6f0..fa53a2301c 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -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 } } diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 09e7e183b9..38c9dd66a8 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -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)) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 1617a354a3..2029e31d4c 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f650e2cbe1..fd0232510a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 522198d0da..d83381f833 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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)?, 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: []) diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index aa2df5f7f6..4c4cc9a152 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -159,7 +159,10 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate count += 1 if count >= maxAnimatedEmojisInText { + #if DEBUG + #else stop = true + #endif } } }