diff --git a/NotificationService/NotificationService.m b/NotificationService/NotificationService.m index 54ced01308..81ba2f0015 100644 --- a/NotificationService/NotificationService.m +++ b/NotificationService/NotificationService.m @@ -63,9 +63,20 @@ static int64_t makePeerId(int32_t namespace, int32_t value) { return self; } +- (void)completeWithContent:(UNNotificationContent * _Nonnull)content { + /*NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"org.telegram.Telegram-iOS.background"]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration]; + NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:@"https://telegram.org"]]; + [task resume];*/ + + if (_contentHandler) { + _contentHandler(content); + } +} + - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { if (_rootPath == nil) { - contentHandler(request.content); + [self completeWithContent:request.content]; return; } @@ -289,8 +300,8 @@ static int64_t makePeerId(int32_t namespace, int32_t value) { _bestAttemptContent.attachments = @[attachment]; } } - if (_contentHandler && _bestAttemptContent != nil) { - _contentHandler(_bestAttemptContent); + if (_bestAttemptContent != nil) { + [self completeWithContent:_bestAttemptContent]; } } else { BuildConfig *buildConfig = [[BuildConfig alloc] initWithBaseAppBundleId:_baseAppBundleId]; @@ -322,21 +333,21 @@ static int64_t makePeerId(int32_t namespace, int32_t value) { } } - if (strongSelf->_contentHandler && strongSelf->_bestAttemptContent != nil) { - strongSelf->_contentHandler(strongSelf->_bestAttemptContent); + if (strongSelf->_bestAttemptContent != nil) { + [strongSelf completeWithContent:strongSelf->_bestAttemptContent]; } } }); }); } } else { - if (_contentHandler && _bestAttemptContent != nil) { - _contentHandler(_bestAttemptContent); + if (_bestAttemptContent != nil) { + [self completeWithContent:_bestAttemptContent]; } } } else { - if (_contentHandler && _bestAttemptContent != nil) { - _contentHandler(_bestAttemptContent); + if (_bestAttemptContent != nil) { + [self completeWithContent:_bestAttemptContent]; } } } @@ -349,7 +360,7 @@ static int64_t makePeerId(int32_t namespace, int32_t value) { if (_contentHandler) { if(_bestAttemptContent) { - _contentHandler(_bestAttemptContent); + [self completeWithContent:_bestAttemptContent]; _bestAttemptContent = nil; } _contentHandler = nil; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index c5d4764928..ecdd070956 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -15,6 +15,7 @@ import TelegramNotices import SearchUI import DeleteChatPeerActionSheetItem import LanguageSuggestionUI +import ContextUI public func useSpecialTabBarIcons() -> Bool { return (Date(timeIntervalSince1970: 1545642000)...Date(timeIntervalSince1970: 1546387200)).contains(Date()) @@ -64,6 +65,27 @@ private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBa return false } +private final class ContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + + init(controller: ViewController, sourceNode: ASDisplayNode?) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerTakeControllerInfo? { + let sourceNode = self.sourceNode + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in + if let sourceNode = sourceNode { + return (sourceNode, sourceNode.bounds) + } else { + return nil + } + }) + } +} + public class ChatListControllerImpl: TelegramBaseController, ChatListController, UIViewControllerPreviewingDelegate { private var validLayout: ContainerViewLayout? @@ -356,6 +378,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, preconditionFailure("debug tap") } } + + self.customPresentPreviewingController = { [weak self] controller, sourceNode in + guard let strongSelf = self else { + return nil + } + var items: [ContextMenuItem] = [] + for i in 0 ..< 5 { + items.append(.action(ContextMenuActionItem(text: "Item \(i)", icon: { _ in nil }, action: { controller, f in + f(.default) + }))) + } + return ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: controller, sourceNode: sourceNode)), items: items, reactionItems: []) + } } required public init(coder aDecoder: NSCoder) { @@ -1068,6 +1103,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return true }) } + + if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { + if !self.didSetup3dTouch { + self.didSetup3dTouch = true + self.registerForPreviewingNonNative(with: self, sourceView: self.view, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme)) + } + } } override public func viewWillDisappear(_ animated: Bool) { diff --git a/submodules/ContextUI/Sources/ContextContentContainerNode.swift b/submodules/ContextUI/Sources/ContextContentContainerNode.swift index 4f850c33d4..3c4a43ca89 100644 --- a/submodules/ContextUI/Sources/ContextContentContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextContentContainerNode.swift @@ -9,7 +9,7 @@ final class ContextContentContainerNode: ASDisplayNode { super.init() } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, scaledSize: CGSize, transition: ContainedViewLayoutTransition) { guard let contentNode = self.contentNode else { return } @@ -17,6 +17,9 @@ final class ContextContentContainerNode: ASDisplayNode { case .extracted: break case let .controller(controller): + transition.updatePosition(node: controller, position: CGPoint(x: scaledSize.width / 2.0, y: scaledSize.height / 2.0)) + transition.updateBounds(node: controller, bounds: CGRect(origin: CGPoint(), size: size)) + transition.updateTransformScale(node: controller, scale: scaledSize.width / size.width) controller.updateLayout(size: size, transition: transition) } } diff --git a/submodules/ContextUI/Sources/ContextContentSourceNode.swift b/submodules/ContextUI/Sources/ContextContentSourceNode.swift index 081d528a26..9f8fe315d8 100644 --- a/submodules/ContextUI/Sources/ContextContentSourceNode.swift +++ b/submodules/ContextUI/Sources/ContextContentSourceNode.swift @@ -38,7 +38,8 @@ final class ContextControllerContentNode: ASDisplayNode { } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size)) + self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) } } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 2637c51cd7..9c69570c7a 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -339,7 +339,25 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * animationDurationFactor, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) } case let .controller(source): - break + let contentParentNode = ContextControllerContentNode(controller: source.controller) + self.contentContainerNode.contentNode = .controller(contentParentNode) + self.contentContainerNode.clipsToBounds = true + self.contentContainerNode.cornerRadius = 14.0 + self.contentContainerNode.addSubnode(contentParentNode) + + let transitionInfo = source.transitionInfo() + if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() { + let projectedFrame = convertFrame(sourceNodeRect, from: sourceNode.view, to: self.view) + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + + var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + self.contentAreaInScreenSpace = updatedContentAreaInScreenSpace + + //self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * animationDurationFactor, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + //self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * animationDurationFactor, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } } if let validLayout = self.validLayout { @@ -375,14 +393,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self?.didCompleteAnimationIn = true }) } - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) let springDuration: Double = 0.42 * animationDurationFactor let springDamping: CGFloat = 104.0 - self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) if let contentNode = self.contentContainerNode.contentNode { switch contentNode { case let .extracted(extracted): + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let contentParentNode = extracted let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) @@ -396,8 +415,23 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) contentParentNode.applyAbsoluteOffsetSpring?(-contentContainerOffset.y, springDuration, springDamping) } - case let .controller(controller): - break + case .controller: + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + self.contentContainerNode.allowsGroupOpacity = true + self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor, completion: { [weak self] _ in + self?.contentContainerNode.allowsGroupOpacity = false + }) + self.contentContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let actionsSideInset: CGFloat = 11.0 + + let localSourceFrame = self.view.convert(CGRect(origin: CGPoint(x: actionsSideInset, y: originalProjectedContentViewFrame.1.minY), size: CGSize(width: actionsSideInset, height: originalProjectedContentViewFrame.1.height)), to: self.scrollNode.view) + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y) + self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + } } } } @@ -410,6 +444,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi switch self.source { case let .extracted(source): + guard let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode) = maybeContentNode else { + return + } + let putBackInfo = source.putBack() if putBackInfo == nil { @@ -438,7 +476,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi var completedContentNode = false var completedActionsNode = false - if let putBackInfo = putBackInfo, let contentParentNode = self.contentParentNode, let parentSupernode = contentParentNode.supernode { + if let putBackInfo = putBackInfo, let parentSupernode = contentParentNode.supernode { self.originalProjectedContentViewFrame = (convertFrame(contentParentNode.frame, from: parentSupernode.view, to: self.view), convertFrame(contentParentNode.contentRect, from: contentParentNode.view, to: self.view)) var updatedContentAreaInScreenSpace = putBackInfo.contentAreaInScreenSpace @@ -449,9 +487,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) } - let contentParentNode = self.contentParentNode - - contentParentNode?.willUpdateIsExtractedToContextPreview?(false) + contentParentNode.willUpdateIsExtractedToContextPreview?(false) let intermediateCompletion: () -> Void = { [weak contentParentNode] in if completedEffect && completedContentNode && completedActionsNode { @@ -518,7 +554,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi animateOutToItem = false } - if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentParentNode = self.contentParentNode { + if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) @@ -532,7 +568,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if let reactionContextNode = self.reactionContextNode { reactionContextNode.animateOut(to: CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: contentParentNode.contentRect.size), animatingOutToReaction: self.reactionContextNodeIsAnimatingOut) } - } else if let contentParentNode = self.contentParentNode { + } else { if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree() { self.contentContainerNode.view.addSubview(snapshotView) } @@ -553,7 +589,137 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } case let .controller(source): - break + guard let maybeContentNode = self.contentContainerNode.contentNode, case let .controller(controller) = maybeContentNode else { + return + } + + let transitionInfo = source.transitionInfo() + + if transitionInfo == nil { + result = .dismissWithoutContent + } + + switch result { + case let .custom(value): + switch value { + case let .animated(duration, curve): + transitionDuration = duration + transitionCurve = curve + default: + break + } + default: + break + } + + self.isUserInteractionEnabled = false + self.isAnimatingOut = true + + self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) + + var completedEffect = false + var completedContentNode = false + var completedActionsNode = false + + if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() { + let projectedFrame = convertFrame(sourceNodeRect, from: sourceNode.view, to: self.view) + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + + var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + } + + let intermediateCompletion: () -> Void = { + if completedEffect && completedContentNode && completedActionsNode { + switch result { + case .default, .custom: + break + case .dismissWithoutContent: + break + } + + completion() + } + } + + if #available(iOS 10.0, *) { + if let propertyAnimator = self.propertyAnimator { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + self.propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration, curve: .easeInOut, animations: { [weak self] in + self?.effectView.effect = nil + }) + } + + if let _ = self.propertyAnimator { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor, from: 0.0, to: 0.999, update: { [weak self] value in + (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value + }, completion: { + completedEffect = true + intermediateCompletion() + }) + } + self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.05 * animationDurationFactor, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) + } else { + UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: { + if #available(iOS 9.0, *) { + self.effectView.effect = nil + } else { + self.effectView.alpha = 0.0 + } + }, completion: { _ in + completedEffect = true + intermediateCompletion() + }) + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in + completedActionsNode = true + intermediateCompletion() + }) + self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in + }) + self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.01, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + + let animateOutToItem: Bool + switch result { + case .default, .custom: + animateOutToItem = true + case .dismissWithoutContent: + animateOutToItem = false + } + + if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let actionsSideInset: CGFloat = 11.0 + + let localSourceFrame = self.view.convert(CGRect(origin: CGPoint(x: actionsSideInset, y: originalProjectedContentViewFrame.1.minY), size: CGSize(width: actionsSideInset, height: originalProjectedContentViewFrame.1.height)), to: self.scrollNode.view) + + self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) + let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y) + self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { _ in + completedContentNode = true + intermediateCompletion() + }) + } else { + if let snapshotView = controller.view.snapshotContentTree() { + self.contentContainerNode.view.addSubview(snapshotView) + } + + self.contentContainerNode.allowsGroupOpacity = true + self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false, completion: { _ in + completedContentNode = true + intermediateCompletion() + }) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.animateOut(to: nil, animatingOutToReaction: self.reactionContextNodeIsAnimatingOut) + } + } } } @@ -646,55 +812,115 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } let actionsBottomInset: CGFloat = 11.0 - if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentParentNode = self.contentParentNode { - let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero - let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - - let actionsSize = self.actionsContainerNode.updateLayout(constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) - let contentSize = originalProjectedContentViewFrame.1.size - self.contentContainerNode.updateLayout(size: contentSize, transition: transition) - - let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) - var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - actionsSize.width - actionsSideInset, originalProjectedContentViewFrame.1.minX)), y: min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin)), size: actionsSize) - var originalContentFrame = CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalActionsFrame.minY - contentActionsSpacing - originalProjectedContentViewFrame.1.size.height), size: originalProjectedContentViewFrame.1.size) - let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) - if originalContentFrame.minY < topEdge { - let requiredOffset = topEdge - originalContentFrame.minY - let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) - let offset = min(requiredOffset, availableOffset) - originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) - originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) - } - - let contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) - - let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) - if self.scrollNode.view.contentSize != scrollContentSize { - self.scrollNode.view.contentSize = scrollContentSize - } - - let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) - - let contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY) - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) - - if isInitialLayout { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) - let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - if overflowOffset < 0.0 { - transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) + if let contentNode = self.contentContainerNode.contentNode { + switch contentNode { + case let .extracted(contentParentNode): + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero + let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + + let actionsSize = self.actionsContainerNode.updateLayout(constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) + let contentSize = originalProjectedContentViewFrame.1.size + self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition) + + let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) + var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - actionsSize.width - actionsSideInset, originalProjectedContentViewFrame.1.minX)), y: min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin)), size: actionsSize) + var originalContentFrame = CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalActionsFrame.minY - contentActionsSpacing - originalProjectedContentViewFrame.1.size.height), size: originalProjectedContentViewFrame.1.size) + let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) + if originalContentFrame.minY < topEdge { + let requiredOffset = topEdge - originalContentFrame.minY + let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) + let offset = min(requiredOffset, availableOffset) + originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) + originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) + } + + let contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) + + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + + let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) + + let contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY) + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + + if isInitialLayout { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) + let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + if overflowOffset < 0.0 { + transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) + } + } + + let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) + + contentParentNode.updateAbsoluteRect?(absoluteContentRect, layout.size) + + if let reactionContextNode = self.reactionContextNode { + let insets = layout.insets(options: [.statusBar]) + transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + reactionContextNode.updateLayout(size: layout.size, insets: insets, anchorRect: CGRect(origin: CGPoint(x: absoluteContentRect.minX + contentParentNode.contentRect.minX, y: absoluteContentRect.minY + contentParentNode.contentRect.minY), size: contentParentNode.contentRect.size), transition: transition) + } + } + case let .controller(contentParentNode): + let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) + + contentParentNode.updateLayout(size: layout.size, transition: transition) + + let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero + let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + + let actionsSize = self.actionsContainerNode.updateLayout(constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) + let contentScale = (layout.size.width - actionsSideInset * 2.0) / layout.size.width + let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset + let contentUnscaledSize = CGSize(width: layout.size.width, height: max(400.0, proposedContentHeight)) + let contentSize = CGSize(width: floor(contentUnscaledSize.width * contentScale), height: floor(contentUnscaledSize.height * contentScale)) + + self.contentContainerNode.updateLayout(size: contentUnscaledSize, scaledSize: contentSize, transition: transition) + + let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) + var originalActionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: maximumActionsFrameOrigin), size: actionsSize) + var originalContentFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize) + if originalContentFrame.minY < topEdge { + let requiredOffset = topEdge - originalContentFrame.minY + let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) + let offset = min(requiredOffset, availableOffset) + originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) + originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) + } + + let contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) + + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + + let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) + + let contentContainerFrame = originalContentFrame + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + + if isInitialLayout { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) + let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + if overflowOffset < 0.0 { + transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) + } + } + + let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) + + if let reactionContextNode = self.reactionContextNode { + let insets = layout.insets(options: [.statusBar]) + transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + reactionContextNode.updateLayout(size: layout.size, insets: insets, anchorRect: CGRect(origin: CGPoint(x: absoluteContentRect.minX, y: absoluteContentRect.minY), size: contentSize), transition: transition) } - } - - let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) - - contentParentNode.updateAbsoluteRect?(absoluteContentRect, layout.size) - - if let reactionContextNode = self.reactionContextNode { - let insets = layout.insets(options: [.statusBar]) - transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - reactionContextNode.updateLayout(size: layout.size, insets: insets, anchorRect: CGRect(origin: CGPoint(x: absoluteContentRect.minX + contentParentNode.contentRect.minX, y: absoluteContentRect.minY + contentParentNode.contentRect.minY), size: contentParentNode.contentRect.size), transition: transition) } } @@ -718,7 +944,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } func scrollViewDidScroll(_ scrollView: UIScrollView) { - if let contentParentNode = self.contentParentNode, let layout = self.validLayout { + guard let layout = self.validLayout else { + return + } + if let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode) = maybeContentNode { let contentContainerFrame = self.contentContainerNode.frame contentParentNode.updateAbsoluteRect?(contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y), layout.size) } @@ -734,14 +963,19 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } let mappedPoint = self.view.convert(point, to: self.scrollNode.view) - if let contentParentNode = self.contentParentNode { - let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view) - if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) { - if result is TextSelectionNodeView { - return result - } else if contentParentNode.contentRect.contains(contentPoint) { - return contentParentNode.contentNode.view + if let maybeContentNode = self.contentContainerNode.contentNode { + switch maybeContentNode { + case let .extracted(contentParentNode): + let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view) + if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) { + if result is TextSelectionNodeView { + return result + } else if contentParentNode.contentRect.contains(contentPoint) { + return contentParentNode.contentNode.view + } } + case let .controller(controller): + break } } @@ -765,9 +999,11 @@ public final class ContextControllerTakeViewInfo { public final class ContextControllerTakeControllerInfo { public let contentAreaInScreenSpace: CGRect + public let sourceNode: () -> (ASDisplayNode, CGRect)? - public init(contentAreaInScreenSpace: CGRect) { + public init(contentAreaInScreenSpace: CGRect, sourceNode: @escaping () -> (ASDisplayNode, CGRect)?) { self.contentAreaInScreenSpace = contentAreaInScreenSpace + self.sourceNode = sourceNode } } diff --git a/submodules/Display/Display/PeekControllerGestureRecognizer.swift b/submodules/Display/Display/PeekControllerGestureRecognizer.swift index 505c039ee7..7a4d3c3fd8 100644 --- a/submodules/Display/Display/PeekControllerGestureRecognizer.swift +++ b/submodules/Display/Display/PeekControllerGestureRecognizer.swift @@ -18,7 +18,7 @@ private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> B public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { private let contentAtPoint: (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? - private let present: (PeekControllerContent, ASDisplayNode) -> PeekController? + private let present: (PeekControllerContent, ASDisplayNode) -> ViewController? private let updateContent: (PeekControllerContent?) -> Void private let activateBySingleTap: Bool @@ -36,7 +36,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { private var menuActivation: PeerkControllerMenuActivation? private weak var presentedController: PeekController? - public init(contentAtPoint: @escaping (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, ASDisplayNode) -> PeekController?, updateContent: @escaping (PeekControllerContent?) -> Void = { _ in }, activateBySingleTap: Bool = false) { + public init(contentAtPoint: @escaping (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, ASDisplayNode) -> ViewController?, updateContent: @escaping (PeekControllerContent?) -> Void = { _ in }, activateBySingleTap: Bool = false) { self.contentAtPoint = contentAtPoint self.present = present self.updateContent = updateContent @@ -259,29 +259,35 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { } } else { if let presentedController = strongSelf.present(content, sourceNode) { - if forceActivate { - strongSelf.candidateContent = nil - if case .press = content.menuActivation() { - (presentedController.displayNode as? PeekControllerNode)?.activateMenu() - } - } else { - strongSelf.candidateContent = (sourceNode, content) - strongSelf.menuActivation = content.menuActivation() - strongSelf.presentedController = presentedController - - strongSelf.state = .began - - switch content.menuActivation() { - case .drag: - break - case .press: - if #available(iOSApplicationExtension 9.0, *) { - if presentedController.traitCollection.forceTouchCapability != .available { + if let presentedController = presentedController as? PeekController { + if forceActivate { + strongSelf.candidateContent = nil + if case .press = content.menuActivation() { + (presentedController.displayNode as? PeekControllerNode)?.activateMenu() + } + } else { + strongSelf.candidateContent = (sourceNode, content) + strongSelf.menuActivation = content.menuActivation() + strongSelf.presentedController = presentedController + + strongSelf.state = .began + + switch content.menuActivation() { + case .drag: + break + case .press: + if #available(iOSApplicationExtension 9.0, *) { + if presentedController.traitCollection.forceTouchCapability != .available { + strongSelf.startPressTimer() + } + } else { strongSelf.startPressTimer() } - } else { - strongSelf.startPressTimer() - } + } + } + } else { + if strongSelf.state != .ended { + strongSelf.state = .ended } } } diff --git a/submodules/Display/Display/ViewController.swift b/submodules/Display/Display/ViewController.swift index bdaa155448..561c7c7538 100644 --- a/submodules/Display/Display/ViewController.swift +++ b/submodules/Display/Display/ViewController.swift @@ -224,6 +224,8 @@ open class ViewControllerPresentationArguments { public var scrollToTopWithTabBar: (() -> Void)? public var longTapWithTabBar: (() -> Void)? + public var customPresentPreviewingController: ((ViewController, ASDisplayNode) -> ViewController?)? + open func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { } @@ -491,12 +493,14 @@ open class ViewControllerPresentationArguments { @available(iOSApplicationExtension 9.0, iOS 9.0, *) open func registerForPreviewing(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme, onlyNative: Bool) { - if self.traitCollection.forceTouchCapability == .available { + if false, self.traitCollection.forceTouchCapability == .available { let _ = super.registerForPreviewing(with: delegate, sourceView: sourceView) } else if !onlyNative { if self.previewingContext == nil { let previewingContext = SimulatedViewControllerPreviewing(theme: theme, delegate: delegate, sourceView: sourceView, node: self.displayNode, present: { [weak self] c, a in self?.presentInGlobalOverlay(c, with: a) + }, customPresent: { [weak self] c, n in + return self?.customPresentPreviewingController?(c, n) }) self.previewingContext = previewingContext } @@ -505,10 +509,12 @@ open class ViewControllerPresentationArguments { @available(iOSApplicationExtension 9.0, iOS 9.0, *) public func registerForPreviewingNonNative(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme) { - if self.traitCollection.forceTouchCapability != .available { + if true || self.traitCollection.forceTouchCapability != .available { if self.previewingContext == nil { let previewingContext = SimulatedViewControllerPreviewing(theme: theme, delegate: delegate, sourceView: sourceView, node: self.displayNode, present: { [weak self] c, a in self?.presentInGlobalOverlay(c, with: a) + }, customPresent: { [weak self] c, n in + return self?.customPresentPreviewingController?(c, n) }) self.previewingContext = previewingContext } diff --git a/submodules/Display/Display/ViewControllerPreviewing.swift b/submodules/Display/Display/ViewControllerPreviewing.swift index aa5c8e9937..508d0ef8b6 100644 --- a/submodules/Display/Display/ViewControllerPreviewing.swift +++ b/submodules/Display/Display/ViewControllerPreviewing.swift @@ -5,7 +5,7 @@ import SwiftSignalKit @available(iOSApplicationExtension 9.0, iOS 9.0, *) private final class ViewControllerPeekContent: PeekControllerContent { - private let controller: ViewController + let controller: ViewController private let menu: [PeekControllerMenuItem] init(controller: ViewController) { @@ -101,7 +101,7 @@ final class SimulatedViewControllerPreviewing: NSObject, UIViewControllerPreview var sourceRect: CGRect = CGRect() - init(theme: PeekControllerTheme, delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, node: ASDisplayNode, present: @escaping (ViewController, Any?) -> Void) { + init(theme: PeekControllerTheme, delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, node: ASDisplayNode, present: @escaping (ViewController, Any?) -> Void, customPresent: ((ViewController, ASDisplayNode) -> ViewController?)?) { self.delegateImpl = delegate self.sourceView = sourceView self.node = node @@ -109,11 +109,16 @@ final class SimulatedViewControllerPreviewing: NSObject, UIViewControllerPreview self.recognizer = PeekControllerGestureRecognizer(contentAtPoint: { point in return contentAtPointImpl?(point) }, present: { content, sourceNode in - let controller = PeekController(theme: theme, content: content, sourceNode: { - return sourceNode - }) - present(controller, nil) - return controller + if let content = content as? ViewControllerPeekContent, let controller = customPresent?(content.controller, sourceNode) { + present(controller, nil) + return controller + } else { + let controller = PeekController(theme: theme, content: content, sourceNode: { + return sourceNode + }) + present(controller, nil) + return controller + } }) node.view.addGestureRecognizer(self.recognizer) diff --git a/submodules/TelegramUI/TelegramUI/AppDelegate.swift b/submodules/TelegramUI/TelegramUI/AppDelegate.swift index 36e3489bdb..a4f187f557 100644 --- a/submodules/TelegramUI/TelegramUI/AppDelegate.swift +++ b/submodules/TelegramUI/TelegramUI/AppDelegate.swift @@ -1277,6 +1277,14 @@ final class SharedApplicationContext { self.isInForegroundPromise.set(true) self.isActiveValue = true self.isActivePromise.set(true) + + let configuration = URLSessionConfiguration.background(withIdentifier: "org.telegram.Telegram-iOS.background") + let session = URLSession(configuration: configuration) + if #available(iOS 9.0, *) { + session.getAllTasks(completionHandler: { tasks in + print(tasks) + }) + } } func applicationWillTerminate(_ application: UIApplication) { @@ -1741,6 +1749,11 @@ final class SharedApplicationContext { }) } + func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { + Logger.shared.log("App \(self.episodeId)", "handleEventsForBackgroundURLSession \(identifier)") + completionHandler() + } + override var next: UIResponder? { if let context = self.contextValue, let controller = context.context.keyShortcutsController { return controller diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 44a393b825..d2098f5c24 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -563,7 +563,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if Namespaces.Message.allScheduled.contains(message.id.namespace) { reactionItems = [] } - let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message), items: actions, reactionItems: reactionItems, recognizer: recognizer) + let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message)), items: actions, reactionItems: reactionItems, recognizer: recognizer) strongSelf.currentContextController = controller controller.reactionSelected = { [weak controller] value in guard let strongSelf = self, let message = updatedMessages.first else { @@ -1392,7 +1392,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }))) - let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message), items: actions, reactionItems: [], recognizer: nil) + let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message)), items: actions, reactionItems: [], recognizer: nil) strongSelf.currentContextController = controller strongSelf.window?.presentInGlobalOverlay(controller) }) @@ -1432,7 +1432,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G f(.dismissWithoutContent) }))) - let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message), items: actions, reactionItems: [], recognizer: nil) + let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message)), items: actions, reactionItems: [], recognizer: nil) strongSelf.currentContextController = controller strongSelf.window?.presentInGlobalOverlay(controller) }) diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift index 4d23ae3178..b01bd7c434 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -832,7 +832,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } strongSelf.fetchDisposable.set(visibilityAwareFetchSignal.start()) } - } else if case .prefetch = automaticDownload, message.id.namespace != Namespaces.Message.SecretIncoming && message.id.namespace != Namespaces.Message.Local { + } else if case .prefetch = automaticDownload, message.id.namespace != Namespaces.Message.SecretIncoming /*&& message.id.namespace != Namespaces.Message.Local*/ { if let file = media as? TelegramMediaFile { let fetchSignal = preloadVideoResource(postbox: context.account.postbox, resourceReference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), duration: 4.0) let visibilityAwareFetchSignal = strongSelf.visibilityPromise.get() diff --git a/submodules/TelegramUI/TelegramUI/FetchVideoMediaResource.swift b/submodules/TelegramUI/TelegramUI/FetchVideoMediaResource.swift index 75f16189fc..7f12ab1f79 100644 --- a/submodules/TelegramUI/TelegramUI/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/TelegramUI/FetchVideoMediaResource.swift @@ -73,6 +73,151 @@ struct VideoConversionConfiguration { } } +private final class FetchVideoLibraryMediaResourceItem { + let priority: MediaBoxFetchPriority + let signal: Signal + let next: (MediaResourceDataFetchResult) -> Void + let error: (MediaResourceDataFetchError) -> Void + let completion: () -> Void + var isActive: Bool = false + var disposable: Disposable? + + init(priority: MediaBoxFetchPriority, signal: Signal, next: @escaping (MediaResourceDataFetchResult) -> Void, error: @escaping (MediaResourceDataFetchError) -> Void, completion: @escaping () -> Void) { + self.priority = priority + self.signal = signal + self.next = next + self.error = error + self.completion = completion + } + + deinit { + self.disposable?.dispose() + } +} + +private let fetchVideoLimit: Int = 2 + +private final class FetchVideoLibraryMediaResourceContextImpl { + private let queue: Queue + var items: [FetchVideoLibraryMediaResourceItem] = [] + + init(queue: Queue) { + self.queue = queue + } + + func add(priority: MediaBoxFetchPriority, signal: Signal, next: @escaping (MediaResourceDataFetchResult) -> Void, error: @escaping (MediaResourceDataFetchError) -> Void, completion: @escaping () -> Void) -> Disposable { + let queue = self.queue + + let item = FetchVideoLibraryMediaResourceItem(priority: priority, signal: signal, next: next, error: error, completion: completion) + self.items.append(item) + + self.update() + + return ActionDisposable { [weak self, weak item] in + queue.async { + guard let strongSelf = self, let item = item else { + return + } + for i in 0 ..< strongSelf.items.count { + if strongSelf.items[i] === item { + strongSelf.items[i].disposable?.dispose() + strongSelf.items.remove(at: i) + strongSelf.update() + break + } + } + } + } + } + + func update() { + let queue = self.queue + + var activeCount = 0 + for item in self.items { + if item.isActive { + activeCount += 1 + } + } + + while activeCount < fetchVideoLimit { + var maxPriorityIndex: Int? + for i in 0 ..< self.items.count { + if !self.items[i].isActive { + if let maxPriorityIndexValue = maxPriorityIndex { + if self.items[i].priority.rawValue > self.items[maxPriorityIndexValue].priority.rawValue { + maxPriorityIndex = i + } + } else { + maxPriorityIndex = i + } + } + } + if let maxPriorityIndex = maxPriorityIndex { + let item = self.items[maxPriorityIndex] + item.isActive = true + activeCount += 1 + assert(item.disposable == nil) + item.disposable = self.items[maxPriorityIndex].signal.start(next: { [weak item] value in + queue.async { + item?.next(value) + } + }, error: { [weak item] value in + queue.async { + item?.error(value) + } + }, completed: { [weak self, weak item] in + queue.async { + guard let strongSelf = self, let item = item else { + return + } + for i in 0 ..< strongSelf.items.count { + if strongSelf.items[i] === item { + strongSelf.items.remove(at: i) + item.completion() + strongSelf.update() + break + } + } + } + }) + } else { + break + } + } + } +} + +private final class FetchVideoLibraryMediaResourceContext { + private let queue = Queue() + private let impl: QueueLocalObject + + init() { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return FetchVideoLibraryMediaResourceContextImpl(queue: queue) + }) + } + + func wrap(priority: MediaBoxFetchPriority, signal: Signal) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.add(priority: priority, signal: signal, next: { value in + subscriber.putNext(value) + }, error: { error in + subscriber.putError(error) + }, completion: { + subscriber.putCompletion() + })) + } + return disposable + } + } +} + +private let throttlingContext = FetchVideoLibraryMediaResourceContext() + public func fetchVideoLibraryMediaResource(postbox: Postbox, resource: VideoLibraryMediaResource) -> Signal { return postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> take(1) @@ -82,9 +227,8 @@ public func fetchVideoLibraryMediaResource(postbox: Postbox, resource: VideoLibr |> introduceError(MediaResourceDataFetchError.self) |> mapToSignal { appConfiguration -> Signal in let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration) - return Signal { subscriber in + let signal = Signal { subscriber in subscriber.putNext(.reset) - let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [resource.localIdentifier], options: nil) var requestId: PHImageRequestID? let disposable = MetaDisposable() @@ -189,6 +333,7 @@ public func fetchVideoLibraryMediaResource(postbox: Postbox, resource: VideoLibr disposable.dispose() } } + return throttlingContext.wrap(priority: .default, signal: signal) } } @@ -201,7 +346,7 @@ func fetchLocalFileVideoMediaResource(postbox: Postbox, resource: LocalFileVideo |> introduceError(MediaResourceDataFetchError.self) |> mapToSignal { appConfiguration -> Signal in let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration) - return Signal { subscriber in + let signal = Signal { subscriber in subscriber.putNext(.reset) let avAsset = AVURLAsset(url: URL(fileURLWithPath: resource.path)) @@ -267,6 +412,7 @@ func fetchLocalFileVideoMediaResource(postbox: Postbox, resource: LocalFileVideo disposable.dispose() } } + return throttlingContext.wrap(priority: .default, signal: signal) } }