From b3e0d64b3741d229d964aed269ba1105b4ae7b08 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 29 Oct 2024 00:40:32 +0100 Subject: [PATCH] Video improvements --- .../Source/Components/RoundedRectangle.swift | 23 ++- submodules/GalleryUI/BUILD | 3 + .../Sources/GalleryControllerNode.swift | 2 +- .../GalleryUI/Sources/GalleryItemNode.swift | 15 ++ .../GalleryUI/Sources/GalleryPagerNode.swift | 148 +++++++++++++++--- .../GalleryRateToastAnimationComponent.swift | 97 ++++++++++++ .../Sources/GalleryRateToastComponent.swift | 122 +++++++++++++++ .../Items/UniversalVideoGalleryItem.swift | 99 +++++++++++- submodules/TelegramApi/Sources/Api0.swift | 4 +- submodules/TelegramApi/Sources/Api23.swift | 18 +-- submodules/TelegramApi/Sources/Api25.swift | 36 +++-- ...eoChatExpandedSpeakingToastComponent.swift | 2 +- .../Sources/VideoChatScreen.swift | 2 +- .../Account/AccountIntermediateState.swift | 30 +++- .../State/AccountStateManagementUtils.swift | 41 ++++- .../Sources/State/AccountStateManager.swift | 55 +++++++ .../State/SynchronizePeerReadState.swift | 4 +- .../Messages/TelegramEngineMessages.swift | 4 + .../TelegramEngine/Payments/Stars.swift | 3 +- .../Sources/ChatMessageBubbleItemNode.swift | 6 +- .../VideoRateToast.imageset/Contents.json | 12 ++ .../VideoRateToast.imageset/play.pdf | 73 +++++++++ .../Chat/ChatControllerLoadDisplayNode.swift | 68 ++------ ...ChatControllerOpenMessageContextMenu.swift | 11 +- .../Sources/Chat/ChatControllerToasts.swift | 101 ++++++++++++ .../TelegramUI/Sources/ChatController.swift | 79 +--------- .../ChatControllerForwardMessages.swift | 18 --- .../Sources/ChatHistoryListNode.swift | 3 + .../ChatInterfaceStateContextMenus.swift | 32 +++- .../Sources/HLSVideoJSNativeContentNode.swift | 9 ++ submodules/UndoUI/BUILD | 1 + .../Sources/UndoOverlayController.swift | 1 + .../Sources/UndoOverlayControllerNode.swift | 73 ++++++++- 33 files changed, 966 insertions(+), 229 deletions(-) create mode 100644 submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift create mode 100644 submodules/GalleryUI/Sources/GalleryRateToastComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift 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 85bf17faf3..ee73053b7d 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 2b5a7bb6d8..ab271c55da 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -902,7 +902,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1401868056] = { return Api.StarsSubscription.parse_starsSubscription($0) } dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) } dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) } - dict[-1216644148] = { return Api.StarsTransaction.parse_starsTransaction($0) } + dict[903148150] = { return Api.StarsTransaction.parse_starsTransaction($0) } dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) } dict[-110658899] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAPI($0) } dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) } @@ -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/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 179c33dc08..83c674e9d3 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -1010,13 +1010,13 @@ public extension Api { } public extension Api { enum StarsTransaction: TypeConstructorDescription { - case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?, stargift: Api.StarGift?, floodskipDate: Int32?, floodskipNumber: Int32?) + case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?, stargift: Api.StarGift?, floodskipNumber: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipDate, let floodskipNumber): + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipNumber): if boxed { - buffer.appendInt32(-1216644148) + buffer.appendInt32(903148150) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(id, buffer: buffer, boxed: false) @@ -1038,7 +1038,6 @@ public extension Api { if Int(flags) & Int(1 << 12) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {serializeInt32(giveawayPostId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 14) != 0 {stargift!.serialize(buffer, true)} - if Int(flags) & Int(1 << 15) != 0 {serializeInt32(floodskipDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 15) != 0 {serializeInt32(floodskipNumber!, buffer: buffer, boxed: false)} break } @@ -1046,8 +1045,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipDate, let floodskipNumber): - return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any), ("stargift", stargift as Any), ("floodskipDate", floodskipDate as Any), ("floodskipNumber", floodskipNumber as Any)]) + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipNumber): + return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any), ("stargift", stargift as Any), ("floodskipNumber", floodskipNumber as Any)]) } } @@ -1094,8 +1093,6 @@ public extension Api { } } var _17: Int32? if Int(_1!) & Int(1 << 15) != 0 {_17 = reader.readInt32() } - var _18: Int32? - if Int(_1!) & Int(1 << 15) != 0 {_18 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1113,9 +1110,8 @@ public extension Api { let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil let _c16 = (Int(_1!) & Int(1 << 14) == 0) || _16 != nil let _c17 = (Int(_1!) & Int(1 << 15) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 15) == 0) || _18 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 { - return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16, floodskipDate: _17, floodskipNumber: _18) + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { + return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16, floodskipNumber: _17) } else { return nil 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/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 82a91ebb7b..d6b1a54276 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -490,8 +490,7 @@ private final class StarsContextImpl { private extension StarsContext.State.Transaction { init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) { switch apiTransaction { - case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift, floodskipDate, floodskipNumber): - let _ = floodskipDate + case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift, floodskipNumber): let _ = floodskipNumber let parsedPeer: StarsContext.State.Transaction.Peer diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 88243dae3e..8e3a683b14 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -6118,14 +6118,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 dda2573b82..2add8acaa7 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1272,7 +1272,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 @@ -1302,7 +1301,6 @@ extension ChatControllerImpl { } return ids } - stayInThisChat = true } else { var transformedMessages = transformedMessages if shouldDivert { @@ -1334,56 +1332,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 - ) - }) - } } } }) @@ -4659,7 +4607,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, @@ -4993,6 +4941,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 c06d8f3b3b..fb2aa15bba 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -654,6 +654,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, @@ -7194,6 +7197,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.recorderDataDisposable.dispose() self.displaySendWhenOnlineTipDisposable.dispose() self.networkSpeedEventsDisposable?.dispose() + self.postedScheduledMessagesEventsDisposable?.dispose() } deallocate() } @@ -9136,52 +9140,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) { @@ -9240,30 +9198,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 { @@ -10381,10 +10316,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 5d53b73751..39eff054fe 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -3467,6 +3467,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 072162d89b..e0bd6dd81e 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) }