diff --git a/submodules/Display/Source/ImageNode.swift b/submodules/Display/Source/ImageNode.swift index 5d61c11813e..2831794d2fe 100644 --- a/submodules/Display/Source/ImageNode.swift +++ b/submodules/Display/Source/ImageNode.swift @@ -115,6 +115,39 @@ public struct ImageCorners: Equatable { public func withRemovedTails() -> ImageCorners { return ImageCorners(topLeft: self.topLeft.withoutTail, topRight: self.topRight.withoutTail, bottomLeft: self.bottomLeft.withoutTail, bottomRight: self.bottomRight.withoutTail) } + + public var layerCorners: (radius: CGFloat, cornerMask: CACornerMask)? { + guard case .Corner(let topLeftRadius) = topLeft, + case .Corner(let topRightRadius) = topRight, + case .Corner(let bottomLeftRadius) = bottomLeft, + case .Corner(let bottomRightRadius) = bottomRight + else { return nil } + + let maxRadius = max(topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius) + if maxRadius.isZero { return (.zero, []) } + + guard topLeftRadius.isZero || topLeftRadius == maxRadius, + topRightRadius.isZero || topRightRadius == maxRadius, + bottomLeftRadius.isZero || bottomLeftRadius == maxRadius, + bottomRightRadius.isZero || bottomRightRadius == maxRadius + else { return nil } + + var maskedCorners: CACornerMask = [] + if !topLeftRadius.isZero { + maskedCorners.insert(.layerMinXMinYCorner) + } + if !topRightRadius.isZero { + maskedCorners.insert(.layerMaxXMinYCorner) + } + if !bottomLeftRadius.isZero { + maskedCorners.insert(.layerMaxXMinYCorner) + } + if !bottomRightRadius.isZero { + maskedCorners.insert(.layerMaxXMaxYCorner) + } + + return (maxRadius, maskedCorners) + } } public func ==(lhs: ImageCorners, rhs: ImageCorners) -> Bool { diff --git a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift index 31ad26614bd..77a0dfd655f 100644 --- a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift @@ -394,6 +394,7 @@ public class MediaDustNode: ASDisplayNode { } public func update(size: CGSize, color: UIColor, transition: ContainedViewLayoutTransition) { + guard size != self.currentParams?.size || color != self.currentParams?.color else { return } self.currentParams = (size, color) let bounds = CGRect(origin: .zero, size: size) diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index c830fe9deb2..4a8a2d57c20 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -1162,7 +1162,9 @@ public func chatSecretPhoto(account: Account, userLocation: MediaResourceUserLoc adjustSaturationInContext(context: context, saturation: 1.7) - addCorners(context, arguments: arguments) + if !arguments.corners.isEmpty { + addCorners(context, arguments: arguments) + } return context } @@ -2154,7 +2156,9 @@ public func chatSecretMessageVideo(account: Account, userLocation: MediaResource } } - addCorners(context, arguments: arguments) + if !arguments.corners.isEmpty { + addCorners(context, arguments: arguments) + } return context } @@ -2969,7 +2973,9 @@ public func playerAlbumArt(postbox: Postbox, engine: TelegramEngine, fileReferen } } - addCorners(context, arguments: arguments) + if !arguments.corners.isEmpty { + addCorners(context, arguments: arguments) + } return context } diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift index 440082d6683..5edfd87d030 100644 --- a/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift @@ -870,7 +870,7 @@ public final class SemanticStatusNode: ASControlNode { guard let strongSelf = self, ready else { return } - let context = transform(TransformImageArguments(corners: ImageCorners(radius: strongSelf.bounds.width / 2.0), imageSize: strongSelf.bounds.size, boundingSize: strongSelf.bounds.size, intrinsicInsets: UIEdgeInsets())) + let context = transform(TransformImageArguments(corners: ImageCorners(), imageSize: strongSelf.bounds.size, boundingSize: strongSelf.bounds.size, intrinsicInsets: UIEdgeInsets())) let previousAppearanceContext = strongSelf.appearanceContext strongSelf.appearanceContext = strongSelf.appearanceContext.withUpdatedBackgroundImage(context?.generateImage()) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index fa3012d9bc8..570a7c22eb3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -53,6 +53,52 @@ private final class ChatMessageBubbleClippingNode: ASDisplayNode { } } +private final class DisplaySynchronizer { + + private var uiTasks: [() -> Void] = [] + + private lazy var displayLink: CADisplayLink = { + let displayLink = CADisplayLink(target: self, selector: #selector(performUITasks)) + if #available(iOS 15.0, *), UIScreen.main.maximumFramesPerSecond > 60 { + displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0) + } + displayLink.isPaused = true + displayLink.add(to: .main, forMode: .common) + return displayLink + }() + + deinit { + self.displayLink.invalidate() + } + + @objc private func performUITasks() { + guard !self.uiTasks.isEmpty else { + self.displayLink.isPaused = true + return + } + + let allowedDuration = self.displayLink.duration / 2.0 + let deadline = self.displayLink.targetTimestamp + var hasExecuted = false + + while !self.uiTasks.isEmpty, !hasExecuted || CACurrentMediaTime() + allowedDuration < deadline { + let task = self.uiTasks.removeFirst() + Queue.mainQueue().async(task) + + hasExecuted = true + } + } + + fileprivate func scheduleUITask(synchronously: Bool, task: @escaping () -> Void) { + if synchronously { + task() + } else { + self.uiTasks.append(task) + self.displayLink.isPaused = false + } + } +} + func hasCommentButton(item: ChatMessageItem) -> Bool { let firstMessage = item.content.firstMessage @@ -539,6 +585,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode private var currentSwipeAction: ChatControllerInteractionSwipeAction? + private static let synchronizer = DisplaySynchronizer() + //private let debugNode: ASDisplayNode override var visibility: ListViewItemNodeVisibility { @@ -1071,8 +1119,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode for contentNode in self.contentNodes { if let message = contentNode.item?.message { currentContentClassesPropertiesAndLayouts.append((message, type(of: contentNode) as AnyClass, contentNode.supportsMosaic, contentNode.asyncLayoutContent())) - } else { - assertionFailure() } } @@ -2452,51 +2498,53 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let disablesComments = !hasInstantVideo return (layout, { animation, applyInfo, synchronousLoads in - return ChatMessageBubbleItemNode.applyLayout(selfReference: selfReference, animation, synchronousLoads, - params: params, - applyInfo: applyInfo, - layout: layout, - item: item, - forwardSource: forwardSource, - forwardAuthorSignature: forwardAuthorSignature, - accessibilityData: accessibilityData, - actionButtonsSizeAndApply: actionButtonsSizeAndApply, - reactionButtonsSizeAndApply: reactionButtonsSizeAndApply, - updatedMergedTop: updatedMergedTop, - updatedMergedBottom: updatedMergedBottom, - hideBackground: hideBackground, - incoming: incoming, - graphics: graphics, - presentationContext: item.controllerInteraction.presentationContext, - bubbleContentWidth: bubbleContentWidth, - backgroundFrame: backgroundFrame, - deliveryFailedInset: deliveryFailedInset, - nameNodeSizeApply: nameNodeSizeApply, - contentOrigin: contentOrigin, - nameNodeOriginY: nameNodeOriginY, - layoutConstants: layoutConstants, - currentCredibilityIcon: currentCredibilityIcon, - adminNodeSizeApply: adminNodeSizeApply, - contentUpperRightCorner: contentUpperRightCorner, - threadInfoSizeApply: threadInfoSizeApply, - threadInfoOriginY: threadInfoOriginY, - forwardInfoSizeApply: forwardInfoSizeApply, - forwardInfoOriginY: forwardInfoOriginY, - replyInfoSizeApply: replyInfoSizeApply, - replyInfoOriginY: replyInfoOriginY, - removedContentNodeIndices: removedContentNodeIndices, - addedContentNodes: addedContentNodes, - contentNodeMessagesAndClasses: contentNodeMessagesAndClasses, - contentNodeFramesPropertiesAndApply: contentNodeFramesPropertiesAndApply, - contentContainerNodeFrames: contentContainerNodeFrames, - mosaicStatusOrigin: mosaicStatusOrigin, - mosaicStatusSizeAndApply: mosaicStatusSizeAndApply, - needsShareButton: needsShareButton, - shareButtonOffset: shareButtonOffset, - avatarOffset: avatarOffset, - hidesHeaders: hidesHeaders, - disablesComments: disablesComments - ) + Self.synchronizer.scheduleUITask(synchronously: synchronousLoads || animation.isAnimated) { + ChatMessageBubbleItemNode.applyLayout(selfReference: selfReference, animation, synchronousLoads, + params: params, + applyInfo: applyInfo, + layout: layout, + item: item, + forwardSource: forwardSource, + forwardAuthorSignature: forwardAuthorSignature, + accessibilityData: accessibilityData, + actionButtonsSizeAndApply: actionButtonsSizeAndApply, + reactionButtonsSizeAndApply: reactionButtonsSizeAndApply, + updatedMergedTop: updatedMergedTop, + updatedMergedBottom: updatedMergedBottom, + hideBackground: hideBackground, + incoming: incoming, + graphics: graphics, + presentationContext: item.controllerInteraction.presentationContext, + bubbleContentWidth: bubbleContentWidth, + backgroundFrame: backgroundFrame, + deliveryFailedInset: deliveryFailedInset, + nameNodeSizeApply: nameNodeSizeApply, + contentOrigin: contentOrigin, + nameNodeOriginY: nameNodeOriginY, + layoutConstants: layoutConstants, + currentCredibilityIcon: currentCredibilityIcon, + adminNodeSizeApply: adminNodeSizeApply, + contentUpperRightCorner: contentUpperRightCorner, + threadInfoSizeApply: threadInfoSizeApply, + threadInfoOriginY: threadInfoOriginY, + forwardInfoSizeApply: forwardInfoSizeApply, + forwardInfoOriginY: forwardInfoOriginY, + replyInfoSizeApply: replyInfoSizeApply, + replyInfoOriginY: replyInfoOriginY, + removedContentNodeIndices: removedContentNodeIndices, + addedContentNodes: addedContentNodes, + contentNodeMessagesAndClasses: contentNodeMessagesAndClasses, + contentNodeFramesPropertiesAndApply: contentNodeFramesPropertiesAndApply, + contentContainerNodeFrames: contentContainerNodeFrames, + mosaicStatusOrigin: mosaicStatusOrigin, + mosaicStatusSizeAndApply: mosaicStatusSizeAndApply, + needsShareButton: needsShareButton, + shareButtonOffset: shareButtonOffset, + avatarOffset: avatarOffset, + hidesHeaders: hidesHeaders, + disablesComments: disablesComments + ) + } }) } @@ -3096,15 +3144,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - assert(sortedContentNodes.count == updatedContentNodes.count) - strongSelf.contentNodes = sortedContentNodes } var shouldClipOnTransitions = true var contentNodeIndex = 0 for (relativeFrame, _, useContentOrigin, apply) in contentNodeFramesPropertiesAndApply { - apply(animation, synchronousLoads, applyInfo) + Self.synchronizer.scheduleUITask(synchronously: synchronousLoads || animation.isAnimated) { + apply(animation, synchronousLoads, applyInfo) + } if contentNodeIndex >= strongSelf.contentNodes.count { break diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index c241ca3da9c..b484c297ebd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -174,6 +174,23 @@ extension UIBezierPath { } } +private let chatMediaProcessingQueue = Queue(queue: DispatchQueue(label: "ChatMediaProcessingQueue", + qos: .utility, + attributes: .concurrent)) + +private func runOnMediaQueue(attemptSynchronously: Bool = false) -> (Signal) -> Signal { + if attemptSynchronously { + return { signal in + return signal + } + } else { + return { signal in + return signal + |> runOn(chatMediaProcessingQueue) + } + } +} + private class ExtendedMediaOverlayNode: ASDisplayNode { private let blurredImageNode: TransformImageNode private let dustNode: MediaDustNode @@ -280,8 +297,26 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.randomId = randomId self.blurredImageNode.setSignal(imageSignal, attemptSynchronously: true) + let imageCorners: ImageCorners + if let (radius, maskedCorners) = corners?.layerCorners { + imageCorners = ImageCorners() + + self.blurredImageNode.layer.masksToBounds = true + self.blurredImageNode.layer.cornerRadius = radius + self.blurredImageNode.layer.maskedCorners = maskedCorners + if #available(iOS 13.0, *) { + self.blurredImageNode.layer.cornerCurve = .continuous + } + } else { + imageCorners = corners ?? ImageCorners() + + self.blurredImageNode.layer.masksToBounds = false + self.blurredImageNode.layer.cornerRadius = .zero + self.blurredImageNode.layer.maskedCorners = [] + } + let imageLayout = self.blurredImageNode.asyncLayout() - let arguments = TransformImageArguments(corners: corners ?? ImageCorners(), imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .blurBackground, emptyColor: .clear, custom: nil) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .blurBackground, emptyColor: .clear, custom: nil) let apply = imageLayout(arguments) apply() } @@ -967,14 +1002,17 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } if isSecretMedia { updateImageSignal = { synchronousLoad, _ in - return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image)) + return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad) + |> runOnMediaQueue(attemptSynchronously: synchronousLoad) } } else { updateImageSignal = { synchronousLoad, highQuality in return chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality) + |> runOnMediaQueue(attemptSynchronously: synchronousLoad) } updateBlurredImageSignal = { synchronousLoad, _ in - return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), ignoreFullSize: true, synchronousLoad: true) + return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), ignoreFullSize: true, synchronousLoad: synchronousLoad) + |> runOnMediaQueue(attemptSynchronously: synchronousLoad) } } @@ -999,6 +1037,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } updateImageSignal = { synchronousLoad, _ in return chatWebFileImage(account: context.account, file: image) + |> runOnMediaQueue(attemptSynchronously: synchronousLoad) } updatedFetchControls = FetchControls(fetch: { _ in @@ -1011,25 +1050,30 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else if let file = media as? TelegramMediaFile { if isSecretMedia { updateImageSignal = { synchronousLoad, _ in - return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: synchronousLoad) + |> runOnMediaQueue(attemptSynchronously: synchronousLoad) } } else { if file.isAnimatedSticker { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) updateImageSignal = { synchronousLoad, _ in return chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) + |> runOnMediaQueue(attemptSynchronously: synchronousLoad) } } else if file.isSticker || file.isVideoSticker { updateImageSignal = { synchronousLoad, _ in return chatMessageSticker(account: context.account, userLocation: .peer(message.id.peerId), file: file, small: false) + |> runOnMediaQueue(attemptSynchronously: synchronousLoad) } } else { onlyFullSizeVideoThumbnail = isSendingUpdated updateImageSignal = { synchronousLoad, _ in return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) + |> runOnMediaQueue(attemptSynchronously: synchronousLoad) } updateBlurredImageSignal = { synchronousLoad, _ in - return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true) + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: synchronousLoad) + |> runOnMediaQueue(attemptSynchronously: synchronousLoad) } } } @@ -1384,7 +1428,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let updatedStatusSignal = updatedStatusSignal { strongSelf.statusDisposable.set((updatedStatusSignal - |> deliverOnMainQueue).start(next: { [weak strongSelf] status, actualFetchStatus in + |> runOnMediaQueue(attemptSynchronously: synchronousLoads) + |> deliverOnMainQueue + ).start(next: { [weak strongSelf] status, actualFetchStatus in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { strongSelf.fetchStatus = status @@ -1397,7 +1443,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let updatedVideoNodeReadySignal = updatedVideoNodeReadySignal { strongSelf.videoNodeReadyDisposable.set((updatedVideoNodeReadySignal - |> deliverOnMainQueue).start(next: { [weak strongSelf] status in + |> runOnMediaQueue(attemptSynchronously: synchronousLoads) + |> deliverOnMainQueue + ).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf, let videoNode = strongSelf.videoNode { strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) @@ -1408,7 +1456,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let updatedPlayerStatusSignal = updatedPlayerStatusSignal { strongSelf.playerStatusDisposable.set((updatedPlayerStatusSignal - |> deliverOnMainQueue).start(next: { [weak strongSelf] status in + |> runOnMediaQueue(attemptSynchronously: synchronousLoads) + |> deliverOnMainQueue + ).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { strongSelf.playerStatus = status @@ -1906,7 +1956,6 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio self.extendedMediaOverlayNode = extendedMediaOverlayNode self.pinchContainerNode.contentNode.insertSubnode(extendedMediaOverlayNode, aboveSubnode: self.imageNode) } - self.extendedMediaOverlayNode?.frame = self.imageNode.frame var tappable = false switch state { @@ -1932,7 +1981,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio break } } - self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: paymentText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners) + + if self.extendedMediaOverlayNode?.frame != self.imageNode.frame { + self.extendedMediaOverlayNode?.frame = self.imageNode.frame + self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: paymentText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners) + } } else if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { self.extendedMediaOverlayNode = nil extendedMediaOverlayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak extendedMediaOverlayNode] _ in diff --git a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift index 46b641a0e2f..178dd646465 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/ChatBubbleVideoDecoration.swift @@ -38,10 +38,15 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration { } public func updateCorners(_ corners: ImageCorners) { - if isRoundEqualCorners(corners) { - self.contentContainerNode.cornerRadius = corners.topLeft.radius + if let (radius, cornerMask) = corners.layerCorners { + self.contentContainerNode.layer.cornerRadius = radius + self.contentContainerNode.layer.maskedCorners = cornerMask self.contentContainerNode.layer.mask = nil + if #available(iOS 13.0, *) { + self.contentContainerNode.layer.cornerCurve = .continuous + } } else { + // Else branch should not be called at all self.contentContainerNode.cornerRadius = 0 let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius))