diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index e4141d06fa..7a3caa1c4e 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -126,13 +126,18 @@ public final class RoundedRectangle: Component { } public final class FilledRoundedRectangleComponent: Component { + public enum CornerRadius: Equatable { + case value(CGFloat) + case minEdge + } + public let color: UIColor - public let cornerRadius: CGFloat + public let cornerRadius: CornerRadius public let smoothCorners: Bool public init( color: UIColor, - cornerRadius: CGFloat, + cornerRadius: CornerRadius, smoothCorners: Bool ) { self.color = color @@ -216,9 +221,17 @@ public final class FilledRoundedRectangleComponent: Component { transition.setTintColor(view: self, color: component.color) - if self.currentCornerRadius != component.cornerRadius { + let cornerRadius: CGFloat + switch component.cornerRadius { + case let .value(value): + cornerRadius = value + case .minEdge: + cornerRadius = min(availableSize.width, availableSize.height) * 0.5 + } + + if self.currentCornerRadius != cornerRadius { let previousCornerRadius = self.currentCornerRadius - self.currentCornerRadius = component.cornerRadius + self.currentCornerRadius = cornerRadius if transition.animation.isImmediate { self.applyStaticCornerRadius() } else { @@ -236,7 +249,7 @@ public final class FilledRoundedRectangleComponent: Component { } } - transition.setCornerRadius(layer: self.layer, cornerRadius: component.cornerRadius, completion: { [weak self] completed in + transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in guard let self, completed else { return } diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 61c6de7119..9398674cc6 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -57,6 +57,9 @@ swift_library( "//submodules/TelegramUI/Components/SaveProgressScreen", "//submodules/TelegramUI/Components/RasterizedCompositionComponent", "//submodules/TelegramUI/Components/BadgeComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/ComponentDisplayAdapters", "//submodules/ComponentFlow", ], visibility = [ diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 8947584ff1..d269eddc5e 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -303,7 +303,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture self.pager.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: layout.size) - self.pager.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + self.pager.containerLayoutUpdated(layout, navigationBarHeight: self.areControlsHidden ? 0.0 : navigationBarHeight, transition: transition) } open func setControlsHidden(_ hidden: Bool, animated: Bool) { diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 51b9630b0a..1e3d02dbf1 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -11,6 +11,11 @@ public enum GalleryItemNodeNavigationStyle { } open class GalleryItemNode: ASDisplayNode { + public enum ActiveEdge { + case left + case right + } + private var _index: Int? public var index: Int { get { @@ -111,4 +116,14 @@ open class GalleryItemNode: ASDisplayNode { open var keyShortcuts: [KeyShortcut] { return [] } + + open func hasActiveEdgeAction(edge: ActiveEdge) -> Bool { + return false + } + + open func setActiveEdgeAction(edge: ActiveEdge?) { + } + + open func adjustActiveEdgeAction(distance: CGFloat) { + } } diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 59bfe42de1..01d821e7c5 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -9,6 +9,10 @@ private func edgeWidth(width: CGFloat) -> CGFloat { return min(44.0, floor(width / 6.0)) } +private func activeEdgeWidth(width: CGFloat) -> CGFloat { + return floor(width * 0.4) +} + let fadeWidth: CGFloat = 70.0 private let leftFadeImage = generateImage(CGSize(width: fadeWidth, height: 32.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -85,6 +89,9 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest private let leftFadeNode: ASDisplayNode private let rightFadeNode: ASDisplayNode private var highlightedSide: Bool? + private var activeSide: Bool? + private var canPerformSideNavigationAction: Bool = false + private var sideActionInitialPosition: CGPoint? private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? @@ -118,6 +125,8 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest public var pagingEnabledPromise = Promise(true) private var pagingEnabledDisposable: Disposable? + private var edgeLongTapTimer: Foundation.Timer? + public init(pageGap: CGFloat, disableTapNavigation: Bool) { self.pageGap = pageGap self.disableTapNavigation = disableTapNavigation @@ -170,57 +179,99 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest recognizer.delegate = self.wrappedGestureRecognizerDelegate self.tapRecognizer = recognizer recognizer.tapActionAtPoint = { [weak self] point in - guard let strongSelf = self, strongSelf.pagingEnabled else { + guard let strongSelf = self else { return .fail } let size = strongSelf.bounds var highlightedSide: Bool? - if point.x < edgeWidth(width: size.width) && strongSelf.canGoToPreviousItem() { - if strongSelf.items.count > 1 { - highlightedSide = false + var activeSide: Bool? + if point.x < edgeWidth(width: size.width) { + if strongSelf.canGoToPreviousItem() { + if strongSelf.items.count > 1 { + highlightedSide = false + } } - } else if point.x > size.width - edgeWidth(width: size.width) && strongSelf.canGoToNextItem() { - if strongSelf.items.count > 1 { - if point.y < 80.0 { - highlightedSide = nil - } else { + } else if point.x > size.width - edgeWidth(width: size.width) { + if strongSelf.canGoToNextItem() { + if strongSelf.items.count > 1 { highlightedSide = true } } } + if point.x < activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) { + activeSide = false + } + } else if point.x > size.width - activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) { + activeSide = true + } + } - if highlightedSide == nil { + if !strongSelf.pagingEnabled { + highlightedSide = nil + } + + if highlightedSide == nil && activeSide == nil { return .fail } if let result = strongSelf.hitTest(point, with: nil), let _ = result.asyncdisplaykit_node as? ASButtonNode { return .fail } - return .keepWithSingleTap + + if activeSide != nil { + return .waitForHold(timeout: 0.3, acceptTap: true) + } else { + return .keepWithSingleTap + } } recognizer.highlight = { [weak self] point in - guard let strongSelf = self, strongSelf.pagingEnabled else { + guard let strongSelf = self else { return } let size = strongSelf.bounds var highlightedSide: Bool? - if let point = point { - if point.x < edgeWidth(width: size.width) && strongSelf.canGoToPreviousItem() { - if strongSelf.items.count > 1 { - highlightedSide = false + var activeSide: Bool? + if let point { + if point.x < edgeWidth(width: size.width) { + if strongSelf.canGoToPreviousItem() { + if strongSelf.items.count > 1 { + highlightedSide = false + } } - } else if point.x > size.width - edgeWidth(width: size.width) && strongSelf.canGoToNextItem() { - if strongSelf.items.count > 1 { - highlightedSide = true + } else if point.x > size.width - edgeWidth(width: size.width) { + if strongSelf.canGoToNextItem() { + if strongSelf.items.count > 1 { + highlightedSide = true + } + } + } + if point.x < activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) { + activeSide = false + } + } else if point.x > size.width - activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) { + activeSide = true } } } + + if !strongSelf.pagingEnabled { + highlightedSide = nil + } + if strongSelf.highlightedSide != highlightedSide { strongSelf.highlightedSide = highlightedSide + if highlightedSide != nil { + strongSelf.canPerformSideNavigationAction = true + } + let leftAlpha: CGFloat let rightAlpha: CGFloat if let highlightedSide = highlightedSide { @@ -247,6 +298,47 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest } } } + + if strongSelf.activeSide != activeSide { + strongSelf.activeSide = activeSide + + if let activeSide, let centralIndex = strongSelf.centralItemIndex, let _ = strongSelf.visibleItemNode(at: centralIndex) { + if strongSelf.edgeLongTapTimer == nil { + strongSelf.edgeLongTapTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false, block: { _ in + guard let self else { + return + } + if let centralIndex = self.centralItemIndex, let itemNode = self.visibleItemNode(at: centralIndex) { + itemNode.setActiveEdgeAction(edge: activeSide ? .right : .left) + } + + self.canPerformSideNavigationAction = false + + let leftAlpha: CGFloat + let rightAlpha: CGFloat + + leftAlpha = 0.0 + rightAlpha = 0.0 + + if self.leftFadeNode.alpha != leftAlpha { + self.leftFadeNode.alpha = leftAlpha + self.leftFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) + } + if self.rightFadeNode.alpha != rightAlpha { + self.rightFadeNode.alpha = rightAlpha + self.rightFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) + } + }) + } + } else if let edgeLongTapTimer = strongSelf.edgeLongTapTimer { + edgeLongTapTimer.invalidate() + strongSelf.edgeLongTapTimer = nil + + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex) { + itemNode.setActiveEdgeAction(edge: nil) + } + } + } } self.view.addGestureRecognizer(recognizer) } @@ -258,8 +350,9 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: + self.sideActionInitialPosition = nil if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { - if case .tap = gesture { + if case .tap = gesture, self.canPerformSideNavigationAction { let size = self.bounds.size if location.x < edgeWidth(width: size.width) && self.canGoToPreviousItem() { self.goToPreviousItem() @@ -268,6 +361,21 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest } } } + case .cancelled: + self.sideActionInitialPosition = nil + case .began: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { + self.sideActionInitialPosition = location + } + case .changed: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { + if let sideActionInitialPosition = self.sideActionInitialPosition { + let distance = location.x - sideActionInitialPosition.x + if let centralIndex = self.centralItemIndex, let itemNode = self.visibleItemNode(at: centralIndex) { + itemNode.adjustActiveEdgeAction(distance: distance) + } + } + } default: break } diff --git a/submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift b/submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift new file mode 100644 index 0000000000..6b543c5cc4 --- /dev/null +++ b/submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift @@ -0,0 +1,97 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AppBundle + +final class GalleryRateToastAnimationComponent: Component { + init() { + } + + static func ==(lhs: GalleryRateToastAnimationComponent, rhs: GalleryRateToastAnimationComponent) -> Bool { + return true + } + + final class View: UIView { + private let itemViewContainer: UIView + private var itemViews: [UIImageView] = [] + + override init(frame: CGRect) { + self.itemViewContainer = UIView() + + super.init(frame: frame) + + self.addSubview(self.itemViewContainer) + + let image = UIImage(bundleImageName: "Media Gallery/VideoRateToast")?.withRenderingMode(.alwaysTemplate) + for _ in 0 ..< 2 { + let itemView = UIImageView(image: image) + itemView.tintColor = .white + self.itemViews.append(itemView) + self.itemViewContainer.addSubview(itemView) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupAnimations() { + let beginTime = self.layer.convertTime(CACurrentMediaTime(), from: nil) + + for i in 0 ..< self.itemViews.count { + if self.itemViews[i].layer.animation(forKey: "idle-opacity") != nil { + continue + } + + let delay = Double(i) * 0.1 + + let animation = CABasicAnimation(keyPath: "opacity") + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + animation.beginTime = beginTime + delay + animation.fromValue = 0.6 as NSNumber + animation.toValue = 1.0 as NSNumber + animation.repeatCount = Float.infinity + animation.autoreverses = true + animation.fillMode = .both + animation.duration = 0.4 + self.itemViews[i].layer.add(animation, forKey: "idle-opacity") + + let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") + scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + scaleAnimation.beginTime = beginTime + delay + scaleAnimation.fromValue = 0.9 as NSNumber + scaleAnimation.toValue = 1.1 as NSNumber + scaleAnimation.repeatCount = Float.infinity + scaleAnimation.autoreverses = true + scaleAnimation.fillMode = .both + scaleAnimation.duration = 0.4 + self.itemViews[i].layer.add(scaleAnimation, forKey: "idle-scale") + } + } + + func update(component: GalleryRateToastAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let itemSize = self.itemViews[0].image?.size ?? CGSize(width: 10.0, height: 10.0) + let itemSpacing: CGFloat = 1.0 + + let size = CGSize(width: itemSize.width * 2.0 + itemSpacing, height: 12.0) + + for i in 0 ..< self.itemViews.count { + let itemFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (itemSize.width + itemSpacing), y: UIScreenPixel), size: itemSize) + self.itemViews[i].frame = itemFrame + } + + self.setupAnimations() + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/GalleryUI/Sources/GalleryRateToastComponent.swift b/submodules/GalleryUI/Sources/GalleryRateToastComponent.swift new file mode 100644 index 0000000000..d0a2951526 --- /dev/null +++ b/submodules/GalleryUI/Sources/GalleryRateToastComponent.swift @@ -0,0 +1,122 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AppBundle +import MultilineTextComponent +import AnimatedTextComponent + +final class GalleryRateToastComponent: Component { + let rate: Double + + init(rate: Double) { + self.rate = rate + } + + static func ==(lhs: GalleryRateToastComponent, rhs: GalleryRateToastComponent) -> Bool { + if lhs.rate != rhs.rate { + return false + } + return true + } + + final class View: UIView { + private let background = ComponentView() + private let text = ComponentView() + private let arrows = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: GalleryRateToastComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let insets = UIEdgeInsets(top: 5.0, left: 11.0, bottom: 5.0, right: 16.0) + let spacing: CGFloat = 5.0 + + var rateString = String(format: "%.1f", component.rate) + if rateString.hasSuffix(".0") { + rateString = rateString.replacingOccurrences(of: ".0", with: "") + } + + var textItems: [AnimatedTextComponent.Item] = [] + if let dotRange = rateString.range(of: ".") { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("pre"), content: .text(String(rateString[rateString.startIndex ..< dotRange.lowerBound])))) + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("dot"), content: .text("."))) + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("post"), content: .text(String(rateString[dotRange.upperBound...])))) + } else { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("pre"), content: .text(rateString))) + } + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("x"), content: .text("x"))) + + let textSize = self.text.update( + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.semibold(17.0), + color: .white, + items: textItems + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let arrowsSize = self.arrows.update( + transition: transition, + component: AnyComponent(GalleryRateToastAnimationComponent()), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + + let size = CGSize(width: insets.left + insets.right + textSize.width + arrowsSize.width, height: insets.top + insets.bottom + max(textSize.height, arrowsSize.height)) + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: UIColor(white: 0.0, alpha: 0.5), + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, + containerSize: size + ) + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let textFrame = CGRect(origin: CGPoint(x: insets.left, y: floorToScreenPixels((size.height - textSize.height) * 0.5)), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + textView.layer.anchorPoint = CGPoint() + self.addSubview(textView) + } + transition.setPosition(view: textView, position: textFrame.origin) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + + let arrowsFrame = CGRect(origin: CGPoint(x: textFrame.maxX + spacing, y: floorToScreenPixels((size.height - arrowsSize.height) * 0.5)), size: arrowsSize) + if let arrowsView = self.arrows.view { + if arrowsView.superview == nil { + self.addSubview(arrowsView) + } + transition.setFrame(view: arrowsView, frame: arrowsFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 525b4c1776..7333f1b8f6 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -32,6 +32,7 @@ import SectionTitleContextItem import RasterizedCompositionComponent import BadgeComponent import ComponentFlow +import ComponentDisplayAdapters public enum UniversalVideoGalleryItemContentInfo { case message(Message, Int?) @@ -1318,7 +1319,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var playOnContentOwnership = false private var skipInitialPause = false private var ignorePauseStatus = false - private var validLayout: (ContainerViewLayout, CGFloat)? + private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)? private var didPause = false private var isPaused = true private var dismissOnOrientationChange = false @@ -1368,6 +1369,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var activePictureInPictureNavigationController: NavigationController? private var activePictureInPictureController: ViewController? + private var activeEdgeRateState: (initialRate: Double, currentRate: Double)? + private var activeEdgeRateIndicator: ComponentView? + init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData @@ -1603,6 +1607,46 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { pictureInPictureNode.updateLayout(placeholderSize, transition: transition) } } + + if let activeEdgeRateState = self.activeEdgeRateState { + var activeEdgeRateIndicatorTransition = transition + let activeEdgeRateIndicator: ComponentView + if let current = self.activeEdgeRateIndicator { + activeEdgeRateIndicator = current + } else { + activeEdgeRateIndicator = ComponentView() + self.activeEdgeRateIndicator = activeEdgeRateIndicator + activeEdgeRateIndicatorTransition = .immediate + } + + let activeEdgeRateIndicatorSize = activeEdgeRateIndicator.update( + transition: ComponentTransition(activeEdgeRateIndicatorTransition), + component: AnyComponent(GalleryRateToastComponent( + rate: activeEdgeRateState.currentRate + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + let activeEdgeRateIndicatorFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - activeEdgeRateIndicatorSize.width) * 0.5), y: max(navigationBarHeight, layout.statusBarHeight ?? 0.0) + 8.0), size: activeEdgeRateIndicatorSize) + if let activeEdgeRateIndicatorView = activeEdgeRateIndicator.view { + if activeEdgeRateIndicatorView.superview == nil { + self.view.addSubview(activeEdgeRateIndicatorView) + transition.animateTransformScale(view: activeEdgeRateIndicatorView, from: 0.001) + if transition.isAnimated { + activeEdgeRateIndicatorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + activeEdgeRateIndicatorTransition.updateFrame(view: activeEdgeRateIndicatorView, frame: activeEdgeRateIndicatorFrame) + } + } else if let activeEdgeRateIndicator = self.activeEdgeRateIndicator { + self.activeEdgeRateIndicator = nil + if let activeEdgeRateIndicatorView = activeEdgeRateIndicator.view { + transition.updateAlpha(layer: activeEdgeRateIndicatorView.layer, alpha: 0.0, completion: { [weak activeEdgeRateIndicatorView] _ in + activeEdgeRateIndicatorView?.removeFromSuperview() + }) + transition.updateTransformScale(layer: activeEdgeRateIndicatorView.layer, scale: 0.001) + } + } if dismiss { self.dismiss() @@ -3938,6 +3982,59 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } return keyShortcuts } + + override func hasActiveEdgeAction(edge: ActiveEdge) -> Bool { + if case .right = edge { + return true + } else { + return false + } + } + + override func setActiveEdgeAction(edge: ActiveEdge?) { + guard let videoNode = self.videoNode else { + return + } + if let edge, case .right = edge { + let effectiveRate: Double + if let current = self.activeEdgeRateState { + effectiveRate = min(2.5, current.initialRate + 0.5) + self.activeEdgeRateState = (current.initialRate, effectiveRate) + } else { + guard let playbackRate = self.playbackRate else { + return + } + effectiveRate = min(2.5, playbackRate + 0.5) + self.activeEdgeRateState = (playbackRate, effectiveRate) + } + videoNode.setBaseRate(effectiveRate) + } else if let (initialRate, _) = self.activeEdgeRateState { + self.activeEdgeRateState = nil + videoNode.setBaseRate(initialRate) + } + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.35, curve: .spring)) + } + } + + override func adjustActiveEdgeAction(distance: CGFloat) { + guard let videoNode = self.videoNode else { + return + } + if let current = self.activeEdgeRateState { + var rateFraction = Double(distance) / 100.0 + rateFraction = max(0.0, min(1.0, rateFraction)) + let rateDistance = (current.initialRate + 0.5) * (1.0 - rateFraction) + 2.5 * rateFraction + let effectiveRate = max(1.0, min(2.5, rateDistance)) + self.activeEdgeRateState = (current.initialRate, effectiveRate) + videoNode.setBaseRate(effectiveRate) + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.35, curve: .spring)) + } + } + } } final class HeaderContextReferenceContentSource: ContextReferenceContentSource { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 117a4753a6..ab271c55da 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -1005,7 +1005,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1576161051] = { return Api.Update.parse_updateDeleteMessages($0) } dict[1407644140] = { return Api.Update.parse_updateDeleteQuickReply($0) } dict[1450174413] = { return Api.Update.parse_updateDeleteQuickReplyMessages($0) } - dict[-1870238482] = { return Api.Update.parse_updateDeleteScheduledMessages($0) } + dict[-223929981] = { return Api.Update.parse_updateDeleteScheduledMessages($0) } dict[654302845] = { return Api.Update.parse_updateDialogFilter($0) } dict[-1512627963] = { return Api.Update.parse_updateDialogFilterOrder($0) } dict[889491791] = { return Api.Update.parse_updateDialogFilters($0) } diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index a758260b09..2c19d44fd9 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -676,7 +676,7 @@ public extension Api { case updateDeleteMessages(messages: [Int32], pts: Int32, ptsCount: Int32) case updateDeleteQuickReply(shortcutId: Int32) case updateDeleteQuickReplyMessages(shortcutId: Int32, messages: [Int32]) - case updateDeleteScheduledMessages(peer: Api.Peer, messages: [Int32]) + case updateDeleteScheduledMessages(flags: Int32, peer: Api.Peer, messages: [Int32], sentMessages: [Int32]?) case updateDialogFilter(flags: Int32, id: Int32, filter: Api.DialogFilter?) case updateDialogFilterOrder(order: [Int32]) case updateDialogFilters @@ -1246,16 +1246,22 @@ public extension Api { serializeInt32(item, buffer: buffer, boxed: false) } break - case .updateDeleteScheduledMessages(let peer, let messages): + case .updateDeleteScheduledMessages(let flags, let peer, let messages, let sentMessages): if boxed { - buffer.appendInt32(-1870238482) + buffer.appendInt32(-223929981) } + serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(messages.count)) for item in messages { serializeInt32(item, buffer: buffer, boxed: false) } + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sentMessages!.count)) + for item in sentMessages! { + serializeInt32(item, buffer: buffer, boxed: false) + }} break case .updateDialogFilter(let flags, let id, let filter): if boxed { @@ -2097,8 +2103,8 @@ public extension Api { return ("updateDeleteQuickReply", [("shortcutId", shortcutId as Any)]) case .updateDeleteQuickReplyMessages(let shortcutId, let messages): return ("updateDeleteQuickReplyMessages", [("shortcutId", shortcutId as Any), ("messages", messages as Any)]) - case .updateDeleteScheduledMessages(let peer, let messages): - return ("updateDeleteScheduledMessages", [("peer", peer as Any), ("messages", messages as Any)]) + case .updateDeleteScheduledMessages(let flags, let peer, let messages, let sentMessages): + return ("updateDeleteScheduledMessages", [("flags", flags as Any), ("peer", peer as Any), ("messages", messages as Any), ("sentMessages", sentMessages as Any)]) case .updateDialogFilter(let flags, let id, let filter): return ("updateDialogFilter", [("flags", flags as Any), ("id", id as Any), ("filter", filter as Any)]) case .updateDialogFilterOrder(let order): @@ -3309,18 +3315,26 @@ public extension Api { } } public static func parse_updateDeleteScheduledMessages(_ reader: BufferReader) -> Update? { - var _1: Api.Peer? + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer + _2 = Api.parse(reader, signature: signature) as? Api.Peer } - var _2: [Int32]? + var _3: [Int32]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + _3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } + var _4: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.Update.updateDeleteScheduledMessages(peer: _1!, messages: _2!) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.Update.updateDeleteScheduledMessages(flags: _1!, peer: _2!, messages: _3!, sentMessages: _4) } else { return nil diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift index 8170418a48..d50ae11f7f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift @@ -105,7 +105,7 @@ final class VideoChatExpandedSpeakingToastComponent: Component { transition: transition, component: AnyComponent(FilledRoundedRectangleComponent( color: UIColor(white: 0.0, alpha: 0.9), - cornerRadius: size.height * 0.5, + cornerRadius: .value(size.height * 0.5), smoothCorners: false )), environment: {}, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index f49f82e495..751c4db8b9 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1224,7 +1224,7 @@ final class VideoChatScreenComponent: Component { )), background: AnyComponent(FilledRoundedRectangleComponent( color: UIColor(white: 1.0, alpha: 0.1), - cornerRadius: navigationButtonDiameter * 0.5, + cornerRadius: .value(navigationButtonDiameter * 0.5), smoothCorners: false )), effectAlignment: .center, diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index fbf747c2cf..8b304e8311 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -212,6 +212,7 @@ struct AccountMutableState { var namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]] var updatedOutgoingUniqueMessageIds: [Int64: Int32] var storedStories: [StoryId: UpdatesStoredStory] + var sentScheduledMessageIds: Set var resetForumTopicLists: [PeerId: StateResetForumTopics] = [:] @@ -231,7 +232,7 @@ struct AccountMutableState { var authorizationListUpdated: Bool = false - init(initialState: AccountInitialState, initialPeers: [PeerId: Peer], initialReferencedReplyMessageIds: ReferencedReplyMessageIds, initialReferencedGeneralMessageIds: Set, initialStoredMessages: Set, initialStoredStories: [StoryId: UpdatesStoredStory], initialReadInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set]) { + init(initialState: AccountInitialState, initialPeers: [PeerId: Peer], initialReferencedReplyMessageIds: ReferencedReplyMessageIds, initialReferencedGeneralMessageIds: Set, initialStoredMessages: Set, initialStoredStories: [StoryId: UpdatesStoredStory], initialReadInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], initialSentScheduledMessageIds: Set) { self.initialState = initialState self.state = initialState.state self.peers = initialPeers @@ -240,6 +241,7 @@ struct AccountMutableState { self.referencedGeneralMessageIds = initialReferencedGeneralMessageIds self.storedMessages = initialStoredMessages self.storedStories = initialStoredStories + self.sentScheduledMessageIds = initialSentScheduledMessageIds self.readInboxMaxIds = initialReadInboxMaxIds self.channelStates = initialState.channelStates self.peerChatInfos = initialState.peerChatInfos @@ -249,7 +251,7 @@ struct AccountMutableState { self.updatedOutgoingUniqueMessageIds = [:] } - init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, storedStories: [StoryId: UpdatesStoredStory], readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) { + init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, storedStories: [StoryId: UpdatesStoredStory], sentScheduledMessageIds: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) { self.initialState = initialState self.operations = operations self.state = state @@ -260,6 +262,7 @@ struct AccountMutableState { self.referencedGeneralMessageIds = referencedGeneralMessageIds self.storedMessages = storedMessages self.storedStories = storedStories + self.sentScheduledMessageIds = sentScheduledMessageIds self.peerChatInfos = peerChatInfos self.readInboxMaxIds = readInboxMaxIds self.storedMessagesByPeerIdAndTimestamp = storedMessagesByPeerIdAndTimestamp @@ -271,7 +274,7 @@ struct AccountMutableState { } func branch() -> AccountMutableState { - return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, storedStories: self.storedStories, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count) + return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, storedStories: self.storedStories, sentScheduledMessageIds: self.sentScheduledMessageIds, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count) } mutating func merge(_ other: AccountMutableState) { @@ -282,6 +285,8 @@ struct AccountMutableState { self.storedStories[id] = story } + self.sentScheduledMessageIds.formUnion(other.sentScheduledMessageIds) + for i in other.branchOperationIndex ..< other.operations.count { self.addOperation(other.operations[i]) } @@ -357,6 +362,10 @@ struct AccountMutableState { self.addOperation(.DeleteMessages(messageIds)) } + mutating func addSentScheduledMessageIds(_ messageIds: [MessageId]) { + self.sentScheduledMessageIds.formUnion(messageIds) + } + mutating func editMessage(_ id: MessageId, message: StoreMessage) { self.addOperation(.EditMessage(id, message)) } @@ -842,13 +851,15 @@ struct AccountReplayedFinalState { let updatedRevenueBalances: [PeerId: RevenueStats.Balances] let updatedStarsBalance: [PeerId: Int64] let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] + let sentScheduledMessageIds: Set } struct AccountFinalStateEvents { let addedIncomingMessageIds: [MessageId] let addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] - let wasScheduledMessageIds:[MessageId] + let wasScheduledMessageIds: [MessageId] let deletedMessageIds: [DeletedMessageId] + let sentScheduledMessageIds: Set let updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] let updatedWebpages: [MediaId: TelegramMediaWebpage] let updatedCalls: [Api.PhoneCall] @@ -873,10 +884,10 @@ struct AccountFinalStateEvents { let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty + return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: Int64] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:]) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: Int64] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set()) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds @@ -903,6 +914,7 @@ struct AccountFinalStateEvents { self.updatedRevenueBalances = updatedRevenueBalances self.updatedStarsBalance = updatedStarsBalance self.updatedStarsRevenueStatus = updatedStarsRevenueStatus + self.sentScheduledMessageIds = sentScheduledMessageIds } init(state: AccountReplayedFinalState) { @@ -932,6 +944,7 @@ struct AccountFinalStateEvents { self.updatedRevenueBalances = state.updatedRevenueBalances self.updatedStarsBalance = state.updatedStarsBalance self.updatedStarsRevenueStatus = state.updatedStarsRevenueStatus + self.sentScheduledMessageIds = state.sentScheduledMessageIds } func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents { @@ -961,6 +974,9 @@ struct AccountFinalStateEvents { let isPremiumUpdated = self.isPremiumUpdated || other.isPremiumUpdated - return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs })) + var sentScheduledMessageIds = self.sentScheduledMessageIds + sentScheduledMessageIds.formUnion(other.sentScheduledMessageIds) + + return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs }), sentScheduledMessageIds: sentScheduledMessageIds) } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 24f62db616..9503219ab2 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -529,7 +529,7 @@ func initialStateWithPeerIds(_ transaction: Transaction, peerIds: Set, a } } - let state = AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedReplyMessageIds: referencedReplyMessageIds, initialReferencedGeneralMessageIds: referencedGeneralMessageIds, initialStoredMessages: storedMessages, initialStoredStories: storedStories, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp) + let state = AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedReplyMessageIds: referencedReplyMessageIds, initialReferencedGeneralMessageIds: referencedGeneralMessageIds, initialStoredMessages: storedMessages, initialStoredStories: storedStories, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp, initialSentScheduledMessageIds: Set()) return state } @@ -1666,12 +1666,19 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum, namespace: Namespaces.Message.QuickReplyCloud) { updatedState.addQuickReplyMessages([message]) } - case let .updateDeleteScheduledMessages(peer, messages): + case let .updateDeleteScheduledMessages(_, peer, messages, sentMessages): var messageIds: [MessageId] = [] + var sentMessageIds: [MessageId] = [] for message in messages { messageIds.append(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.ScheduledCloud, id: message)) } + if let sentMessages { + for message in sentMessages { + sentMessageIds.append(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: message)) + } + } updatedState.deleteMessages(messageIds) + updatedState.addSentScheduledMessageIds(sentMessageIds) case let .updateDeleteQuickReplyMessages(_, messages): var messageIds: [MessageId] = [] for message in messages { @@ -2628,7 +2635,7 @@ func pollChannelOnce(accountPeerId: PeerId, postbox: Postbox, network: Network, peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) } } - let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) + let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:], initialSentScheduledMessageIds: Set()) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) @@ -2685,7 +2692,7 @@ public func standalonePollChannelOnce(accountPeerId: PeerId, postbox: Postbox, n peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) } } - let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) + let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:], initialSentScheduledMessageIds: Set()) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) @@ -5342,5 +5349,29 @@ func replayFinalState( _internal_setStarsReactionDefaultToPrivate(isPrivate: updatedStarsReactionsAreAnonymousByDefault, transaction: transaction) } - return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, addedReactionEvents: addedReactionEvents, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, deletedMessageIds: deletedMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, addedCallSignalingData: addedCallSignalingData, updatedGroupCallParticipants: updatedGroupCallParticipants, storyUpdates: storyUpdates, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: updatedRevenueBalances, updatedStarsBalance: updatedStarsBalance, updatedStarsRevenueStatus: updatedStarsRevenueStatus) + return AccountReplayedFinalState( + state: finalState, + addedIncomingMessageIds: addedIncomingMessageIds, + addedReactionEvents: addedReactionEvents, + wasScheduledMessageIds: wasScheduledMessageIds, + addedSecretMessageIds: addedSecretMessageIds, + deletedMessageIds: deletedMessageIds, + updatedTypingActivities: updatedTypingActivities, + updatedWebpages: updatedWebpages, + updatedCalls: updatedCalls, + addedCallSignalingData: addedCallSignalingData, + updatedGroupCallParticipants: updatedGroupCallParticipants, + storyUpdates: storyUpdates, + updatedPeersNearby: updatedPeersNearby, + isContactUpdates: isContactUpdates, + delayNotificatonsUntil: delayNotificatonsUntil, + updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, + updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, + updateConfig: updateConfig, + isPremiumUpdated: isPremiumUpdated, + updatedRevenueBalances: updatedRevenueBalances, + updatedStarsBalance: updatedStarsBalance, + updatedStarsRevenueStatus: updatedStarsRevenueStatus, + sentScheduledMessageIds: finalState.state.sentScheduledMessageIds + ) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index abaf7e17dd..1fc793db67 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -63,6 +63,7 @@ public enum DeletedMessageId: Hashable { final class MessagesRemovedContext { private var messagesRemovedInteractively = Set() + private var messagesRemovedRemotely = Set() private var messagesRemovedInteractivelyLock = NSLock() func synchronouslyIsMessageDeletedInteractively(ids: [MessageId]) -> [EngineMessage.Id] { @@ -85,6 +86,26 @@ final class MessagesRemovedContext { return result } + func synchronouslyIsMessageDeletedRemotely(ids: [MessageId]) -> [EngineMessage.Id] { + var result: [EngineMessage.Id] = [] + + self.messagesRemovedInteractivelyLock.lock() + for id in ids { + let mappedId: DeletedMessageId + if id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup { + mappedId = .global(id.id) + } else { + mappedId = .messageId(id) + } + if self.messagesRemovedRemotely.contains(mappedId) { + result.append(id) + } + } + self.messagesRemovedInteractivelyLock.unlock() + + return result + } + func addIsMessagesDeletedInteractively(ids: [DeletedMessageId]) { if ids.isEmpty { return @@ -94,6 +115,16 @@ final class MessagesRemovedContext { self.messagesRemovedInteractively.formUnion(ids) self.messagesRemovedInteractivelyLock.unlock() } + + func addIsMessagesDeletedRemotely(ids: [DeletedMessageId]) { + if ids.isEmpty { + return + } + + self.messagesRemovedInteractivelyLock.lock() + self.messagesRemovedRemotely.formUnion(ids) + self.messagesRemovedInteractivelyLock.unlock() + } } public final class AccountStateManager { @@ -297,6 +328,11 @@ public final class AccountStateManager { return self.forceSendPendingStarsReactionPipe.signal() } + fileprivate let sentScheduledMessageIdsPipe = ValuePipe>() + public var sentScheduledMessageIds: Signal, NoError> { + return self.sentScheduledMessageIdsPipe.signal() + } + private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() @@ -690,6 +726,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } return result @@ -835,6 +872,7 @@ public final class AccountStateManager { if let replayedState = replayedState { if !replayedState.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: replayedState.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: replayedState.deletedMessageIds) } return (difference, replayedState, false, false) @@ -973,6 +1011,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } let deltaTime = CFAbsoluteTimeGetCurrent() - startTime @@ -1080,6 +1119,9 @@ public final class AccountStateManager { if !events.updatedIncomingThreadReadStates.isEmpty || !events.updatedOutgoingThreadReadStates.isEmpty { strongSelf.threadReadStateUpdatesPipe.putNext((events.updatedIncomingThreadReadStates, events.updatedOutgoingThreadReadStates)) } + if !events.sentScheduledMessageIds.isEmpty { + strongSelf.sentScheduledMessageIdsPipe.putNext(events.sentScheduledMessageIds) + } if !events.isContactUpdates.isEmpty { strongSelf.addIsContactUpdates(events.isContactUpdates) } @@ -1248,6 +1290,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } return result @@ -1296,6 +1339,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } let deltaTime = CFAbsoluteTimeGetCurrent() - startTime @@ -1388,6 +1432,7 @@ public final class AccountStateManager { if let replayedState = replayedState, !replayedState.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: replayedState.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: replayedState.deletedMessageIds) } let deltaTime = CFAbsoluteTimeGetCurrent() - startTime @@ -1889,6 +1934,12 @@ public final class AccountStateManager { } } + public var sentScheduledMessageIds: Signal, NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.sentScheduledMessageIds.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + func forceSendPendingStarsReaction(messageId: MessageId) { self.impl.with { impl in impl.forceSendPendingStarsReactionPipe.putNext(messageId) @@ -2128,6 +2179,10 @@ public final class AccountStateManager { public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { return self.messagesRemovedContext.synchronouslyIsMessageDeletedInteractively(ids: ids) } + + public func synchronouslyIsMessageDeletedRemotely(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { + return self.messagesRemovedContext.synchronouslyIsMessageDeletedRemotely(ids: ids) + } } func resolveNotificationSettings(list: [TelegramPeerNotificationSettings], defaultSettings: MessageNotificationSettings) -> (sound: PeerMessageSound, notify: Bool, displayContents: Bool) { diff --git a/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift b/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift index bed0706667..b61049a1f4 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift @@ -178,7 +178,9 @@ private func validatePeerReadState(network: Network, postbox: Postbox, stateMana if case let .idBased(updatedMaxIncomingReadId, _, _, updatedCount, updatedMarkedUnread) = readState { if updatedCount != 0 || updatedMarkedUnread { if localMaxIncomingReadId > updatedMaxIncomingReadId { - return .retry + if !"".isEmpty { + return .retry + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 5933679e31..202922f2ab 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1401,6 +1401,10 @@ public extension TelegramEngine { return self.account.stateManager.synchronouslyIsMessageDeletedInteractively(ids: ids) } + public func synchronouslyIsMessageDeletedRemotely(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { + return self.account.stateManager.synchronouslyIsMessageDeletedRemotely(ids: ids) + } + public func synchronouslyLookupCorrelationId(correlationId: Int64) -> EngineMessage.Id? { return self.account.pendingMessageManager.synchronouslyLookupCorrelationId(correlationId: correlationId) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index bc1736a2d2..6bbd6909d7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -6122,14 +6122,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } override public func getStatusNode() -> ASDisplayNode? { + if let statusNode = self.mosaicStatusNode { + return statusNode + } for contentNode in self.contentNodes { if let statusNode = contentNode.getStatusNode() { return statusNode } } - if let statusNode = self.mosaicStatusNode { - return statusNode - } return nil } diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json new file mode 100644 index 0000000000..9c81bdcc4b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "play.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf new file mode 100644 index 0000000000..c3ed904f78 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf @@ -0,0 +1,73 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 1.411133 cm +1.000000 1.000000 1.000000 scn +0.000000 8.588867 m +0.000000 0.588867 l +0.000000 -0.235178 0.940764 -0.705560 1.600000 -0.211133 c +6.933333 3.788867 l +7.466667 4.188867 7.466667 4.988867 6.933333 5.388867 c +1.600000 9.388867 l +0.940764 9.883294 0.000000 9.412912 0.000000 8.588867 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 378 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 8.000000 12.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000468 00000 n +0000000490 00000 n +0000000662 00000 n +0000000736 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +795 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 2f0c0b3223..25ad446c6b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1278,7 +1278,6 @@ extension ChatControllerImpl { let _ = (strongSelf.shouldDivertMessagesToScheduled(messages: transformedMessages) |> deliverOnMainQueue).start(next: { shouldDivert in let signal: Signal<[MessageId?], NoError> - var stayInThisChat = false var shouldOpenScheduledMessages = false if forwardSourcePeerIds.count > 1 { var forwardedMessages = forwardedMessages @@ -1308,7 +1307,6 @@ extension ChatControllerImpl { } return ids } - stayInThisChat = true } else { var transformedMessages = transformedMessages if shouldDivert { @@ -1340,56 +1338,6 @@ extension ChatControllerImpl { strongSelf.layoutActionOnViewTransitionAction = nil layoutActionOnViewTransitionAction() } - - if stayInThisChat { - strongSelf.dismissAllUndoControllers() - - //TODO:localize - strongSelf.present( - UndoOverlayController( - presentationData: strongSelf.presentationData, - content: .info( - title: "Improving video...", - text: "The video will be published after it's optimized for the bese viewing experience.", - timeout: 8.0, - customUndoText: nil - ), - elevatedLayout: false, - position: .top, - action: { _ in - return true - } - ), - in: .current - ) - } else { - strongSelf.openScheduledMessages(force: true, completion: { c in - guard let self else { - return - } - - c.dismissAllUndoControllers() - - //TODO:localize - c.present( - UndoOverlayController( - presentationData: self.presentationData, - content: .info( - title: "Improving video...", - text: "The video will be published after it's optimized for the bese viewing experience.", - timeout: 8.0, - customUndoText: nil - ), - elevatedLayout: false, - position: .top, - action: { _ in - return true - } - ), - in: .current - ) - }) - } } } }) @@ -4665,7 +4613,7 @@ extension ChatControllerImpl { title: "Improving video...", text: "The video will be published after it's optimized for the bese viewing experience.", customUndoText: nil, - timeout: 6.0 + timeout: 3.5 ), elevatedLayout: false, position: .top, @@ -4999,6 +4947,20 @@ extension ChatControllerImpl { }), in: .current) }) + if case .scheduledMessages = self.subject { + self.postedScheduledMessagesEventsDisposable = (self.context.account.stateManager.sentScheduledMessageIds + |> deliverOnMainQueue).start(next: { [weak self] ids in + guard let self, let peerId = self.chatLocation.peerId else { + return + } + let filteredIds = Array(ids).filter({ $0.peerId == peerId }) + if filteredIds.isEmpty { + return + } + self.displayPostedScheduledMessagesToast(ids: filteredIds) + }) + } + self.displayNodeDidLoad() } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index fdb78b8e30..b0d5639d4b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -106,14 +106,9 @@ extension ChatControllerImpl { } } - //TODO:localize - /*if "".isEmpty, let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { - for media in message.media { - if let file = media as? TelegramMediaFile, file.isVideo, !file.isInstantVideo, !file.isAnimated { - tip = .videoProcessing - } - } - }*/ + if messages.contains(where: { $0.pendingProcessingAttribute != nil }) { + tip = .videoProcessing + } if actions.tip == nil { actions.tip = tip diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift new file mode 100644 index 0000000000..6e60a3c266 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift @@ -0,0 +1,101 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import UndoUI +import AccountContext +import ChatControllerInteraction + +extension ChatControllerImpl { + func displayPostedScheduledMessagesToast(ids: [EngineMessage.Id]) { + let timestamp = CFAbsoluteTimeGetCurrent() + if self.lastPostedScheduledMessagesToastTimestamp + 0.4 >= timestamp { + return + } + self.lastPostedScheduledMessagesToastTimestamp = timestamp + + guard case .scheduledMessages = self.presentationInterfaceState.subject else { + return + } + + let _ = (self.context.engine.data.get( + EngineDataList(ids.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] messages in + guard let self else { + return + } + let messages = messages.compactMap { $0 } + + var found: (message: EngineMessage, file: TelegramMediaFile)? + outer: for message in messages { + for media in message.media { + if let file = media as? TelegramMediaFile, file.isVideo { + found = (message, file) + break outer + } + } + } + + guard let (message, file) = found else { + return + } + + guard case let .loaded(isEmpty, _) = self.chatDisplayNode.historyNode.currentHistoryState else { + return + } + + if isEmpty { + if let navigationController = self.navigationController as? NavigationController, let topController = navigationController.viewControllers.first(where: { c in + if let c = c as? ChatController, c.chatLocation == self.chatLocation { + return true + } + return false + }) as? ChatControllerImpl { + topController.controllerInteraction?.presentControllerInCurrent(UndoOverlayController( + presentationData: self.presentationData, + content: .media( + context: self.context, + file: .message(message: MessageReference(message._asMessage()), media: file), + title: nil, + text: "Video Published", + undoText: nil, + customAction: nil + ), + elevatedLayout: false, + position: .top, + animateInAsReplacement: false, + action: { _ in false } + ), nil) + + self.dismiss() + } + } else { + //TODO:localize + self.controllerInteraction?.presentControllerInCurrent(UndoOverlayController( + presentationData: self.presentationData, + content: .media( + context: self.context, + file: .message(message: MessageReference(message._asMessage()), media: file), + title: nil, + text: "Video Published", + undoText: "View", + customAction: { [weak self] in + guard let self else { + return + } + self.dismiss() + } + ), + elevatedLayout: false, + position: .top, + animateInAsReplacement: false, + action: { _ in false } + ), nil) + } + }) + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 739140bb3e..f87e8e55d5 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -656,6 +656,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var layoutActionOnViewTransitionAction: (() -> Void)? + var lastPostedScheduledMessagesToastTimestamp: Double = 0.0 + var postedScheduledMessagesEventsDisposable: Disposable? + public init( context: AccountContext, chatLocation: ChatLocation, @@ -7213,6 +7216,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.recorderDataDisposable.dispose() self.displaySendWhenOnlineTipDisposable.dispose() self.networkSpeedEventsDisposable?.dispose() + self.postedScheduledMessagesEventsDisposable?.dispose() } deallocate() } @@ -9155,52 +9159,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G func shouldDivertMessagesToScheduled(targetPeer: EnginePeer? = nil, messages: [EnqueueMessage]) -> Signal { return .single(false) - /*guard let peer = targetPeer?._asPeer() ?? self.presentationInterfaceState.renderedPeer?.peer else { - return .single(false) - } - - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - } else { - return .single(false) - } - - //TODO:release - if !"".isEmpty { - return .single(false) - } - - var forwardMessageIds: [EngineMessage.Id] = [] - - for message in messages { - if case let .message(_, _, _, mediaReference, _, _, _, _, _, _) = message, let media = mediaReference?.media { - if let file = media as? TelegramMediaFile, file.isVideo && !file.isInstantVideo && !file.isAnimated { - return .single(true) - } - } else if case let .forward(sourceId, _, _, _, _) = message { - forwardMessageIds.append(sourceId) - } - } - - if forwardMessageIds.isEmpty { - return .single(false) - } else { - return self.context.engine.data.get( - EngineDataList(forwardMessageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) - ) - |> map { messages -> Bool in - for message in messages { - guard let message else { - continue - } - for media in message.media { - if let file = media as? TelegramMediaFile, file.isVideo && !file.isInstantVideo && !file.isAnimated { - return true - } - } - } - return false - } - }*/ } func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { @@ -9259,30 +9217,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G layoutActionOnViewTransitionAction() } - self.openScheduledMessages(force: true, completion: { [weak self] c in - guard let self else { - return - } - c.dismissAllUndoControllers() - - //TODO:localize - c.present( - UndoOverlayController( - presentationData: self.presentationData, - content: .info( - title: "Improving video...", - text: "The video will be published after it's optimized for the bese viewing experience.", - timeout: 8.0, - customUndoText: nil - ), - elevatedLayout: false, - position: .top, - action: { _ in - return true - } - ), - in: .current - ) + self.openScheduledMessages(force: true, completion: { _ in }) } } else { @@ -10404,10 +10339,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let itemNode = latestNode, let statusNode = itemNode.getStatusNode() { let bounds = statusNode.view.convert(statusNode.view.bounds, to: self.chatDisplayNode.view) - let location = CGPoint(x: bounds.midX, y: bounds.minY - 11.0) + let location = CGPoint(x: bounds.midX, y: bounds.minY - 8.0) //TODO:localize - let tooltipController = TooltipController(content: .text("Processing video may take a few minutes."), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, balancedTextLayout: true, timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) + let tooltipController = TooltipController(content: .text("Processing video may take a few minutes."), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, balancedTextLayout: true, isBlurred: true, timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) self.checksTooltipController = tooltipController tooltipController.dismissed = { [weak self, weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.checksTooltipController === tooltipController { diff --git a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift index 1992ca77e6..42fb76fdd3 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -257,24 +257,6 @@ extension ChatControllerImpl { }) if displayConvertingTooltip { - //TODO:localize - strongSelf.present( - UndoOverlayController( - presentationData: strongSelf.presentationData, - content: .info( - title: "Improving video...", - text: "The video will be published after it's optimized for the bese viewing experience.", - timeout: 8.0, - customUndoText: nil - ), - elevatedLayout: false, - position: .top, - action: { _ in - return true - } - ), - in: .current - ) } }) } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 585618e109..106debf3c2 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -3470,6 +3470,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } for id in self.context.engine.messages.synchronouslyIsMessageDeletedInteractively(ids: testIds) { + if id.namespace == Namespaces.Message.ScheduledCloud { + continue + } inner: for (stableId, listId) in maybeRemovedInteractivelyMessageIds { if listId == id { expiredMessageStableIds.insert(stableId) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index f7d3ba9d2b..49206a7995 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1137,9 +1137,29 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if data.messageActions.options.contains(.sendScheduledNow) { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.ScheduledMessages_SendNow, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id]) - f(.dismissWithoutContent) + }, action: { c, _ in + if messages.contains(where: { $0.pendingProcessingAttribute != nil }) { + c?.dismiss(completion: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + //TODO:localize + controllerInteraction.presentController(standardTextAlertController( + theme: AlertControllerTheme(presentationData: presentationData), + title: "Wait!", + text: "This video hasn't been converted and optimized yet. If you send it now, the viewers of the video may experience slow download speed.", + actions: [ + TextAlertAction(type: .defaultAction, title: "Send Anyway", action: { + controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id]) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ), nil) + }) + } else { + c?.dismiss(result: .dismissWithoutContent, completion: nil) + controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id]) + } }))) } @@ -2210,8 +2230,10 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer } if id.namespace == Namespaces.Message.ScheduledCloud { optionsMap[id]!.insert(.sendScheduledNow) - if canEditMessage(accountPeerId: accountPeerId, limitsConfiguration: limitsConfiguration, message: message, reschedule: true) { - optionsMap[id]!.insert(.editScheduledTime) + if message.pendingProcessingAttribute == nil { + if canEditMessage(accountPeerId: accountPeerId, limitsConfiguration: limitsConfiguration, message: message, reschedule: true) { + optionsMap[id]!.insert(.editScheduledTime) + } } if let peer = getPeer(id.peerId), let channel = peer as? TelegramChannel { if !message.flags.contains(.Incoming) { diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift index 9963ace1b5..ef59b5b078 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift @@ -1572,6 +1572,15 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod } func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + if self.playerAvailableLevels.isEmpty { + if let qualitySet = HLSQualitySet(baseFile: self.fileReference), let minQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file { + let sortedFiles = qualitySet.qualityFiles.sorted(by: { $0.key > $1.key }) + if let minQuality = sortedFiles.first(where: { $0.value.media.fileId == minQualityFile.media.fileId }) { + return (minQuality.key, .auto, sortedFiles.map(\.key)) + } + } + } + guard let playerCurrentLevelIndex = self.playerCurrentLevelIndex else { return nil } diff --git a/submodules/UndoUI/BUILD b/submodules/UndoUI/BUILD index 059920aa56..66933e09f8 100644 --- a/submodules/UndoUI/BUILD +++ b/submodules/UndoUI/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/PhotoResources", ], visibility = [ "//visibility:public", diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 0a0a443cf5..ca2d9acc38 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -49,6 +49,7 @@ public enum UndoOverlayContent { case premiumPaywall(title: String?, text: String, customUndoText: String?, timeout: Double?, linkAction: ((String) -> Void)?) case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?) case messageTagged(context: AccountContext, isSingleMessage: Bool, customEmoji: TelegramMediaFile, isBuiltinReaction: Bool, customUndoText: String?) + case media(context: AccountContext, file: FileMediaReference, title: String?, text: String, undoText: String?, customAction: (() -> Void)?) } public enum UndoOverlayAction { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 5bac0fdde3..d04b022b4d 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -23,6 +23,7 @@ import TextNodeWithEntities import BundleIconComponent import AnimatedTextComponent import ComponentDisplayAdapters +import PhotoResources final class UndoOverlayControllerNode: ViewControllerTracingNode { private let presentationData: PresentationData @@ -42,6 +43,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var slotMachineNode: SlotMachineAnimationNode? private var stillStickerNode: TransformImageNode? private var stickerImageSize: CGSize? + private var stickerSourceSize: CGSize? private var stickerOffset: CGPoint? private var emojiStatus: ComponentView? private let titleNode: ImmediateTextNode @@ -1297,6 +1299,58 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { displayUndo = false } + case let .media(context, file, title, text, customUndoText, _): + self.avatarNode = nil + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = nil + + let stillStickerNode = TransformImageNode() + + self.stillStickerNode = stillStickerNode + + var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchSignal: Signal? + + updatedImageSignal = mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .other, videoReference: file, onlyFullSize: false, useLargeThumbnail: false, autoFetchFullSizeThumbnail: false) + updatedFetchSignal = nil + self.stickerImageSize = CGSize(width: 30.0, height: 30.0) + self.stickerSourceSize = file.media.dimensions?.cgSize + + if let title = title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } else { + self.titleNode.attributedText = nil + } + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + self.textNode.maximumNumberOfLines = 5 + + if text.contains("](") { + isUserInteractionEnabled = true + } + + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } + self.originalRemainingSeconds = isUserInteractionEnabled ? 5 : 3 + + if let updatedFetchSignal = updatedFetchSignal { + self.fetchResourceDisposable = updatedFetchSignal.start() + } + + if let updatedImageSignal = updatedImageSignal { + stillStickerNode.setSignal(updatedImageSignal) + } } self.remainingSeconds = self.originalRemainingSeconds @@ -1340,7 +1394,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { self.isUserInteractionEnabled = false } - case .sticker, .customEmoji: + case .sticker, .customEmoji, .media: self.isUserInteractionEnabled = displayUndo case .dice: self.panelWrapperNode.clipsToBounds = true @@ -1468,6 +1522,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { let _ = self.action(.undo) } + case let .media(_, _, _, _, _, customAction): + if let customAction = customAction { + customAction() + } else { + let _ = self.action(.undo) + } default: let _ = self.action(.undo) } @@ -1775,7 +1835,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if let stillStickerNode = self.stillStickerNode { let makeImageLayout = stillStickerNode.asyncLayout() - let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: stickerImageSize, boundingSize: stickerImageSize, intrinsicInsets: UIEdgeInsets())) + + var radius: CGFloat = 0.0 + if case .media = self.content { + radius = 6.0 + } + var stickerImageSourceSize = stickerImageSize + if let stickerSourceSize = self.stickerSourceSize { + stickerImageSourceSize = stickerSourceSize.aspectFilled(stickerImageSourceSize) + } + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: stickerImageSourceSize, boundingSize: stickerImageSize, intrinsicInsets: UIEdgeInsets())) let _ = imageApply() transition.updateFrame(node: stillStickerNode, frame: iconFrame) }