diff --git a/Telegram/BUILD b/Telegram/BUILD index 6989b4401b..f16386e42a 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -443,6 +443,7 @@ official_apple_pay_merchants = [ "merchant.org.telegram.billinenet.test", "merchant.org.telegram.billinenet.prod", "merchant.org.telegram.portmone.test", + "merchant.org.telegram.portmone.prod", "merchant.org.telegram.ecommpay.test", ] diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift index c17d759f73..ac550a607d 100644 --- a/Tests/CallUITest/Sources/ViewController.swift +++ b/Tests/CallUITest/Sources/ViewController.swift @@ -29,7 +29,8 @@ public final class ViewController: UIViewController { shortName: "Emma", avatarImage: UIImage(named: "test"), audioOutput: .internalSpeaker, - isMicrophoneMuted: false, + isLocalAudioMuted: false, + isRemoteAudioMuted: false, localVideo: nil, remoteVideo: nil, isRemoteBatteryLow: false @@ -142,30 +143,39 @@ public final class ViewController: UIViewController { self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0)) self.callState.remoteVideo = nil self.callState.localVideo = nil + self.callState.isLocalAudioMuted = false + self.callState.isRemoteBatteryLow = false self.update(transition: .spring(duration: 0.4)) } callScreenView.backAction = { [weak self] in guard let self else { return } - self.callState.isMicrophoneMuted = !self.callState.isMicrophoneMuted - self.update(transition: .spring(duration: 0.4)) + //self.callState.isLocalAudioMuted = !self.callState.isLocalAudioMuted + //self.update(transition: .spring(duration: 0.4)) + self.callScreenView?.beginPictureInPicture() + } + callScreenView.closeAction = { [weak self] in + guard let self else { + return + } + self.callScreenView?.speakerAction?() } } private func update(transition: Transition) { if let (size, insets) = self.currentLayout { - self.update(size: size, insets: insets, transition: transition) + self.update(size: size, insets: insets, interfaceOrientation: self.interfaceOrientation, transition: transition) } } - private func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) { + private func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, transition: Transition) { guard let callScreenView = self.callScreenView else { return } transition.setFrame(view: callScreenView, frame: CGRect(origin: CGPoint(), size: size)) - callScreenView.update(size: size, insets: insets, screenCornerRadius: UIScreen.main.displayCornerRadius, state: self.callState, transition: transition) + callScreenView.update(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: UIScreen.main.displayCornerRadius, state: self.callState, transition: transition) } override public func viewWillLayoutSubviews() { @@ -182,7 +192,7 @@ public final class ViewController: UIViewController { if let currentLayout = self.currentLayout, currentLayout == (size, insets) { } else { self.currentLayout = (size, insets) - self.update(size: size, insets: insets, transition: transition) + self.update(size: size, insets: insets, interfaceOrientation: self.interfaceOrientation, transition: transition) } } diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index a65befe803..bb50b5420b 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -507,7 +507,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS guard let presentationInterfaceState = self.presentationInterfaceState else { return 0.0 } - return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false) + return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) } public func setCaption(_ caption: NSAttributedString?) { diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 9b1887fbee..57e319ce9c 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -49,6 +49,8 @@ private extension Transition.Animation.Curve { switch self { case .easeInOut: return CAMediaTimingFunction(name: .easeInEaseOut) + case .linear: + return CAMediaTimingFunction(name: .linear) case let .custom(a, b, c, d): return CAMediaTimingFunction(controlPoints: a, b, c, d) case .spring: @@ -72,6 +74,7 @@ public struct Transition { public enum Curve { case easeInOut case spring + case linear case custom(Float, Float, Float, Float) public func solve(at offset: CGFloat) -> CGFloat { @@ -80,6 +83,8 @@ public struct Transition { return listViewAnimationCurveEaseInOut(offset) case .spring: return listViewAnimationCurveSystem(offset) + case .linear: + return offset case let .custom(c1x, c1y, c2x, c2y): return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset) } diff --git a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift index 2c50f91083..33adade5ab 100644 --- a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift +++ b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift @@ -7,7 +7,7 @@ public extension Transition.Animation.Curve { init(_ curve: ContainedViewLayoutTransitionCurve) { switch curve { case .linear: - self = .easeInOut + self = .linear case .easeInOut: self = .easeInOut case let .custom(a, b, c, d): @@ -21,6 +21,8 @@ public extension Transition.Animation.Curve { var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve { switch self { + case .linear: + return .linear case .easeInOut: return .easeInOut case .spring: diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 13679a82fd..c5e577943e 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1776,7 +1776,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega } contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight)) - if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { contentUnscaledSize = preferredSize } } else { @@ -1786,7 +1786,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(568.0, proposedContentHeight)) - if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { contentUnscaledSize = preferredSize } } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 0218be517a..9680ab83e2 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -183,7 +183,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.controller.containerLayoutUpdated( ContainerViewLayout( size: size, - metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: parentLayout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), @@ -765,7 +765,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo defaultContentSize.height = min(defaultContentSize.height, 460.0) let contentSize: CGSize - if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { contentSize = preferredSize } else if let storedContentHeight = contentNode.storedContentHeight { contentSize = CGSize(width: defaultContentSize.width, height: storedContentHeight) diff --git a/submodules/Display/Source/ChildWindowHostView.swift b/submodules/Display/Source/ChildWindowHostView.swift index f4b7a998b9..cda3f5178f 100644 --- a/submodules/Display/Source/ChildWindowHostView.swift +++ b/submodules/Display/Source/ChildWindowHostView.swift @@ -69,13 +69,15 @@ public func childWindowHostView(parent: UIView) -> WindowHostView { let hostView = WindowHostView(containerView: view, eventView: view, isRotating: { return false - }, systemUserInterfaceStyle: .single(.light), updateSupportedInterfaceOrientations: { orientations in + }, systemUserInterfaceStyle: .single(.light), currentInterfaceOrientation: { + return .portrait + }, updateSupportedInterfaceOrientations: { orientations in }, updateDeferScreenEdgeGestures: { edges in }, updatePrefersOnScreenNavigationHidden: { value in }) view.updateSize = { [weak hostView] size in - hostView?.updateSize?(size, 0.0) + hostView?.updateSize?(size, 0.0, .portrait) } view.layoutSubviewsEvent = { [weak hostView] in diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index cbdc55b4a2..a5b8eefe16 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -1573,6 +1573,56 @@ public extension ContainedViewLayoutTransition { } } + func updateLineWidth(layer: CAShapeLayer, lineWidth: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + if layer.lineWidth == lineWidth { + completion?(true) + return + } + + switch self { + case .immediate: + layer.removeAnimation(forKey: "lineWidth") + layer.lineWidth = lineWidth + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let fromLineWidth = layer.lineWidth + layer.lineWidth = lineWidth + layer.animate(from: fromLineWidth as NSNumber, to: lineWidth as NSNumber, keyPath: "lineWidth", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateStrokeColor(layer: CAShapeLayer, strokeColor: UIColor, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + if layer.strokeColor.flatMap(UIColor.init(cgColor:)) == strokeColor { + completion?(true) + return + } + + switch self { + case .immediate: + layer.removeAnimation(forKey: "strokeColor") + layer.strokeColor = strokeColor.cgColor + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let fromStrokeColor = layer.strokeColor ?? UIColor.clear.cgColor + layer.strokeColor = strokeColor.cgColor + layer.animate(from: fromStrokeColor, to: strokeColor.cgColor, keyPath: "strokeColor", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + func attachAnimation(view: UIView, id: String, completion: @escaping (Bool) -> Void) { switch self { case .immediate: diff --git a/submodules/Display/Source/ContainerViewLayout.swift b/submodules/Display/Source/ContainerViewLayout.swift index 09552bbdae..8325f28258 100644 --- a/submodules/Display/Source/ContainerViewLayout.swift +++ b/submodules/Display/Source/ContainerViewLayout.swift @@ -23,15 +23,18 @@ public enum ContainerViewLayoutSizeClass { public struct LayoutMetrics: Equatable { public let widthClass: ContainerViewLayoutSizeClass public let heightClass: ContainerViewLayoutSizeClass + public let orientation: UIInterfaceOrientation? - public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass) { + public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass, orientation: UIInterfaceOrientation?) { self.widthClass = widthClass self.heightClass = heightClass + self.orientation = orientation } public init() { self.widthClass = .compact self.heightClass = .compact + self.orientation = nil } } diff --git a/submodules/Display/Source/ContextContentContainerNode.swift b/submodules/Display/Source/ContextContentContainerNode.swift index 1741434fa7..d2e1081db6 100644 --- a/submodules/Display/Source/ContextContentContainerNode.swift +++ b/submodules/Display/Source/ContextContentContainerNode.swift @@ -22,7 +22,7 @@ public final class ContextContentContainerNode: ASDisplayNode { 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) - controller.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + controller.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) } } } diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index 65deb106b9..36c9486c3e 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -925,3 +925,68 @@ public func drawSvgPath(_ context: CGContext, path: StaticString, strokeOnMove: } } } + +public func convertSvgPath(_ path: StaticString) throws -> CGPath { + var index: UnsafePointer = path.utf8Start + let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount) + var currentPoint = CGPoint() + + let result = CGMutablePath() + + while index < end { + let c = index.pointee + index = index.successor() + + if c == 77 { // M + let x = try readCGFloat(&index, end: end, separator: 44) + let y = try readCGFloat(&index, end: end, separator: 32) + + //print("Move to \(x), \(y)") + currentPoint = CGPoint(x: x, y: y) + result.move(to: currentPoint) + } else if c == 76 { // L + let x = try readCGFloat(&index, end: end, separator: 44) + let y = try readCGFloat(&index, end: end, separator: 32) + + //print("Line to \(x), \(y)") + currentPoint = CGPoint(x: x, y: y) + result.addLine(to: currentPoint) + } else if c == 72 { // H + let x = try readCGFloat(&index, end: end, separator: 32) + + //print("Move to \(x), \(y)") + currentPoint = CGPoint(x: x, y: currentPoint.y) + result.addLine(to: currentPoint) + } else if c == 86 { // V + let y = try readCGFloat(&index, end: end, separator: 32) + + //print("Move to \(x), \(y)") + currentPoint = CGPoint(x: currentPoint.x, y: y) + result.addLine(to: currentPoint) + } else if c == 67 { // C + let x1 = try readCGFloat(&index, end: end, separator: 44) + let y1 = try readCGFloat(&index, end: end, separator: 32) + let x2 = try readCGFloat(&index, end: end, separator: 44) + let y2 = try readCGFloat(&index, end: end, separator: 32) + let x = try readCGFloat(&index, end: end, separator: 44) + let y = try readCGFloat(&index, end: end, separator: 32) + + currentPoint = CGPoint(x: x, y: y) + result.addCurve(to: currentPoint, control1: CGPoint(x: x1, y: y1), control2: CGPoint(x: x2, y: y2)) + } else if c == 90 { // Z + if index != end && index.pointee != 32 { + throw ParsingError.Generic + } + } else if c == 83 { // S + if index != end && index.pointee != 32 { + throw ParsingError.Generic + } + } else if c == 32 { // space + continue + } else { + throw ParsingError.Generic + } + } + + return result +} diff --git a/submodules/Display/Source/NativeWindowHostView.swift b/submodules/Display/Source/NativeWindowHostView.swift index 95e15f0569..4e1bd718ce 100644 --- a/submodules/Display/Source/NativeWindowHostView.swift +++ b/submodules/Display/Source/NativeWindowHostView.swift @@ -12,6 +12,24 @@ private let defaultOrientations: UIInterfaceOrientationMask = { } }() +func getCurrentViewInterfaceOrientation(view: UIView) -> UIInterfaceOrientation { + var orientation: UIInterfaceOrientation = .portrait + if #available(iOS 13.0, *) { + if let window = view as? UIWindow { + if let windowScene = window.windowScene { + orientation = windowScene.interfaceOrientation + } + } else { + if let windowScene = view.window?.windowScene { + orientation = windowScene.interfaceOrientation + } + } + } else { + orientation = UIApplication.shared.statusBarOrientation + } + return orientation +} + public enum WindowUserInterfaceStyle { case light case dark @@ -74,7 +92,7 @@ private final class WindowRootViewController: UIViewController, UIViewController private var registeredForPreviewing = false var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)? - var transitionToSize: ((CGSize, Double) -> Void)? + var transitionToSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)? private var _systemUserInterfaceStyle = ValuePromise(ignoreRepeated: true) var systemUserInterfaceStyle: Signal { @@ -182,8 +200,10 @@ private final class WindowRootViewController: UIViewController, UIViewController override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) + + let orientation = getCurrentViewInterfaceOrientation(view: self.view) UIView.performWithoutAnimation { - self.transitionToSize?(size, coordinator.transitionDuration) + self.transitionToSize?(size, coordinator.transitionDuration, orientation) } } @@ -372,18 +392,29 @@ public func nativeWindowHostView() -> (UIWindow & WindowHost, WindowHostView) { rootViewController.view.frame = CGRect(origin: CGPoint(), size: window.bounds.size) rootViewController.viewDidAppear(false) - let hostView = WindowHostView(containerView: rootViewController.view, eventView: window, isRotating: { - return window.isRotating() - }, systemUserInterfaceStyle: rootViewController.systemUserInterfaceStyle, updateSupportedInterfaceOrientations: { orientations in - rootViewController.orientations = orientations - }, updateDeferScreenEdgeGestures: { edges in - rootViewController.gestureEdges = edges - }, updatePrefersOnScreenNavigationHidden: { value in - rootViewController.prefersOnScreenNavigationHidden = value - }) + let hostView = WindowHostView( + containerView: rootViewController.view, + eventView: window, + isRotating: { + return window.isRotating() + }, + systemUserInterfaceStyle: rootViewController.systemUserInterfaceStyle, + currentInterfaceOrientation: { + return getCurrentViewInterfaceOrientation(view: window) + }, + updateSupportedInterfaceOrientations: { orientations in + rootViewController.orientations = orientations + }, + updateDeferScreenEdgeGestures: { edges in + rootViewController.gestureEdges = edges + }, + updatePrefersOnScreenNavigationHidden: { value in + rootViewController.prefersOnScreenNavigationHidden = value + } + ) - rootViewController.transitionToSize = { [weak hostView] size, duration in - hostView?.updateSize?(size, duration) + rootViewController.transitionToSize = { [weak hostView] size, duration, orientation in + hostView?.updateSize?(size, duration, orientation) } window.updateSize = { _ in diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index 1b05d72188..bb30709aa8 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -156,6 +156,7 @@ public final class WindowHostView { public let eventView: UIView public let isRotating: () -> Bool public let systemUserInterfaceStyle: Signal + public let currentInterfaceOrientation: () -> UIInterfaceOrientation let updateSupportedInterfaceOrientations: (UIInterfaceOrientationMask) -> Void let updateDeferScreenEdgeGestures: (UIRectEdge) -> Void @@ -166,7 +167,7 @@ public final class WindowHostView { var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)? var presentNative: ((UIViewController) -> Void)? var nativeController: (() -> UIViewController?)? - var updateSize: ((CGSize, Double) -> Void)? + var updateSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)? var layoutSubviews: (() -> Void)? var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)? var isUpdatingOrientationLayout = false @@ -178,11 +179,12 @@ public final class WindowHostView { var forEachController: (((ContainableController) -> Void) -> Void)? var getAccessibilityElements: (() -> [Any]?)? - init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) { + init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal, currentInterfaceOrientation: @escaping () -> UIInterfaceOrientation, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) { self.containerView = containerView self.eventView = eventView self.isRotating = isRotating self.systemUserInterfaceStyle = systemUserInterfaceStyle + self.currentInterfaceOrientation = currentInterfaceOrientation self.updateSupportedInterfaceOrientations = updateSupportedInterfaceOrientations self.updateDeferScreenEdgeGestures = updateDeferScreenEdgeGestures self.updatePrefersOnScreenNavigationHidden = updatePrefersOnScreenNavigationHidden @@ -220,11 +222,11 @@ public extension UIView { } } -private func layoutMetricsForScreenSize(_ size: CGSize) -> LayoutMetrics { +private func layoutMetricsForScreenSize(size: CGSize, orientation: UIInterfaceOrientation?) -> LayoutMetrics { if size.width > 690.0 && size.height > 650.0 { - return LayoutMetrics(widthClass: .regular, heightClass: .regular) + return LayoutMetrics(widthClass: .regular, heightClass: .regular, orientation: orientation) } else { - return LayoutMetrics(widthClass: .compact, heightClass: .compact) + return LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: orientation) } } @@ -350,11 +352,13 @@ public class Window1 { self.keyboardViewManager = nil } - let isLandscape = boundsSize.width > boundsSize.height + let isLandscape = boundsSize.width > boundsSize.height let safeInsets = self.deviceMetrics.safeInsets(inLandscape: isLandscape) let onScreenNavigationHeight = self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight) - self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(boundsSize), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning) + let orientation: UIInterfaceOrientation = self.hostView.currentInterfaceOrientation() + + self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(size: boundsSize, orientation: orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning) self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) self.presentationContext = PresentationContext() self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost, parentView: self.hostView.containerView) @@ -406,8 +410,8 @@ public class Window1 { self?.presentNative(controller) } - self.hostView.updateSize = { [weak self] size, duration in - self?.updateSize(size, duration: duration) + self.hostView.updateSize = { [weak self] size, duration, orientation in + self?.updateSize(size, duration: duration, orientation: orientation) } self.hostView.layoutSubviews = { [weak self] in @@ -807,14 +811,14 @@ public class Window1 { return self.viewController?.view.hitTest(point, with: event) } - func updateSize(_ value: CGSize, duration: Double) { + func updateSize(_ value: CGSize, duration: Double, orientation: UIInterfaceOrientation) { let transition: ContainedViewLayoutTransition if !duration.isZero { transition = .animated(duration: duration, curve: .easeInOut) } else { transition = .immediate } - self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(value), safeInsets: self.deviceMetrics.safeInsets(inLandscape: value.width > value.height), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) } + self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(size: value, orientation: orientation), safeInsets: self.deviceMetrics.safeInsets(inLandscape: value.width > value.height), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) } if let statusBarHost = self.statusBarHost, !statusBarHost.isApplicationInForeground { self.layoutSubviews(force: true) } @@ -1117,7 +1121,7 @@ public class Window1 { } let previousInputOffset = inputHeightOffsetForLayout(self.windowLayout) - self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(updatingLayout.layout.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight), upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver) + self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(size: updatingLayout.layout.size, orientation: updatingLayout.layout.metrics.orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight), upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver) let childLayout = containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics) let childLayoutUpdated = self.updatedContainerLayout != childLayout diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 72b1666ca6..7d37c4afb5 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2925,7 +2925,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) { let layout = ContainerViewLayout( size: size, - metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact), + metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact, orientation: nil), deviceMetrics: DeviceMetrics(screenSize: size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight, onScreenNavigationHeight: nil), intrinsicInsets: intrinsicInsets, safeInsets: safeInsets, diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index d79143102f..72bd74e0d3 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -2,6 +2,7 @@ import UIKit #else import AppKit +import TGUIKit #endif import CoreMedia import Accelerate diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index 577cb8f2fe..669e689746 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -3,6 +3,7 @@ import Foundation import UIKit #else import AppKit +import TGUIKit #endif import CoreMedia import SwiftSignalKit diff --git a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift index 8b581bc6b6..72eea9f485 100644 --- a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift @@ -3,6 +3,7 @@ import Foundation import UIKit #else import AppKit +import TGUIKit #endif import SwiftSignalKit import Postbox diff --git a/submodules/MetalEngine/Package.swift b/submodules/MetalEngine/Package.swift index 94aa4301f4..ec3838851c 100644 --- a/submodules/MetalEngine/Package.swift +++ b/submodules/MetalEngine/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "MetalEngine", + platforms: [.macOS(.v10_13)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -13,6 +14,7 @@ let package = Package( ], dependencies: [ .package(name: "ShelfPack", path: "../Utils/ShelfPack"), + .package(name: "TGUIKit", path: "../../../../packages/TGUIKit"), // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), ], @@ -21,7 +23,8 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "MetalEngine", - dependencies: [.product(name: "ShelfPack", package: "ShelfPack", condition: nil)], - path: "Sources/MetalEngine"), + dependencies: [.product(name: "ShelfPack", package: "ShelfPack", condition: nil), + .product(name: "TGUIKit", package: "TGUIKit", condition: nil)], + path: "Sources/"), ] ) diff --git a/submodules/MetalEngine/Sources/MetalEngine.swift b/submodules/MetalEngine/Sources/MetalEngine.swift index 3470972cc8..87672cf6b5 100644 --- a/submodules/MetalEngine/Sources/MetalEngine.swift +++ b/submodules/MetalEngine/Sources/MetalEngine.swift @@ -4,8 +4,12 @@ import Metal #if os(iOS) import Display import UIKit +#else +import AppKit +import TGUIKit #endif + import IOSurface import ShelfPack @@ -662,13 +666,6 @@ public final class MetalEngine { fileprivate var computeStates: [ObjectIdentifier: ComputeState] = [:] init?(device: MTLDevice) { - let mainBundle = Bundle(for: Impl.self) - guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else { - return nil - } - guard let bundle = Bundle(path: path) else { - return nil - } self.device = device @@ -677,15 +674,42 @@ public final class MetalEngine { } self.commandQueue = commandQueue - guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { - return nil - } - self.library = library + let library: MTLLibrary? - guard let vertexFunction = library.makeFunction(name: "clearVertex") else { + #if os(iOS) + let mainBundle = Bundle(for: Impl.self) + guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else { return nil } - guard let fragmentFunction = library.makeFunction(name: "clearFragment") else { + guard let bundle = Bundle(path: path) else { + return nil + } + library = try? device.makeDefaultLibrary(bundle: bundle) + #else + let mainBundle = Bundle(for: Impl.self) + guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else { + return nil + } + guard let bundle = Bundle(path: path) else { + return nil + } + guard let path = bundle.path(forResource: "MetalEngineShaders", ofType: "metallib") else { + return nil + } + library = try? device.makeLibrary(URL: .init(fileURLWithPath: path)) + #endif + + + + guard let lib = library else { + return nil + } + self.library = lib + + guard let vertexFunction = lib.makeFunction(name: "clearVertex") else { + return nil + } + guard let fragmentFunction = lib.makeFunction(name: "clearFragment") else { return nil } diff --git a/submodules/Reachability/Sources/Reachability.swift b/submodules/Reachability/Sources/Reachability.swift index 422cbe0cd7..71e291824e 100644 --- a/submodules/Reachability/Sources/Reachability.swift +++ b/submodules/Reachability/Sources/Reachability.swift @@ -84,7 +84,7 @@ private final class WrappedLegacyReachability: NSObject { } } -@available(iOSApplicationExtension 12.0, iOS 12.0, OSX 13.0, *) +@available(iOSApplicationExtension 12.0, iOS 12.0, macOS 14.0, *) private final class PathMonitor { private let queue: Queue private let monitor: NWPathMonitor @@ -133,7 +133,7 @@ private final class PathMonitor { } } -@available(iOSApplicationExtension 12.0, iOS 12.0, OSX 13.0, *) +@available(iOSApplicationExtension 12.0, iOS 12.0, macOS 14.0, *) private final class SharedPathMonitor { static let queue = Queue() static let impl = QueueLocalObject(queue: queue, generate: { @@ -149,7 +149,7 @@ public enum Reachability { } public static var networkType: Signal { - if #available(iOSApplicationExtension 12.0, iOS 12.0, OSX 13.0, *) { + if #available(iOSApplicationExtension 12.0, iOS 12.0, macOS 14.0, *) { return Signal { subscriber in let disposable = MetaDisposable() diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 5fe09436a1..15e0aa720a 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -73,6 +73,8 @@ public final class CallController: ViewController { private let idleTimerExtensionDisposable = MetaDisposable() + public var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)? + public init(sharedContext: SharedAccountContext, account: Account, call: PresentationCall, easyDebugAccess: Bool) { self.sharedContext = sharedContext self.account = account @@ -136,7 +138,16 @@ public final class CallController: ViewController { override public func loadDisplayNode() { if self.sharedContext.immediateExperimentalUISettings.callUIV2 { - self.displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call) + let displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), easyDebugAccess: self.easyDebugAccess, call: self.call) + self.displayNode = displayNode + + displayNode.restoreUIForPictureInPicture = { [weak self] completion in + guard let self, let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture else { + completion(false) + return + } + restoreUIForPictureInPicture(completion) + } } else { self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call) } diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 53d8dbbbb5..d2b07adac5 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -17,6 +17,7 @@ import ImageBlur import TelegramVoip import MetalEngine import DeviceAccess +import LibYuvBinding final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol { private let sharedContext: SharedAccountContext @@ -29,8 +30,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private let callScreen: PrivateCallScreen private var callScreenState: PrivateCallScreen.State? - private var shouldStayHiddenUntilConnection: Bool = false - private var callStartTimestamp: Double? private var callState: PresentationCallState? @@ -47,6 +46,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP var callEnded: ((Bool) -> Void)? var dismissedInteractively: (() -> Void)? var dismissAllTooltips: (() -> Void)? + var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)? private var emojiKey: (data: Data, resolvedKey: [String])? private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)? @@ -67,7 +67,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP presentationData: PresentationData, statusBar: StatusBar, debugInfo: Signal<(String, String), NoError>, - shouldStayHiddenUntilConnection: Bool = false, easyDebugAccess: Bool, call: PresentationCall ) { @@ -80,8 +79,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.containerView = UIView() self.callScreen = PrivateCallScreen() - self.shouldStayHiddenUntilConnection = shouldStayHiddenUntilConnection - super.init() self.view.addSubview(self.containerView) @@ -122,6 +119,20 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } self.back?() + self.callScreen.beginPictureInPictureIfPossible() + } + self.callScreen.closeAction = { [weak self] in + guard let self else { + return + } + self.dismissedInteractively?() + } + self.callScreen.restoreUIForPictureInPicture = { [weak self] completion in + guard let self else { + completion(false) + return + } + self.restoreUIForPictureInPicture?(completion) } self.callScreenState = PrivateCallScreen.State( @@ -130,7 +141,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP shortName: " ", avatarImage: nil, audioOutput: .internalSpeaker, - isMicrophoneMuted: false, + isLocalAudioMuted: false, + isRemoteAudioMuted: false, localVideo: nil, remoteVideo: nil, isRemoteBatteryLow: false @@ -145,8 +157,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } self.isMuted = isMuted - if callScreenState.isMicrophoneMuted != isMuted { - callScreenState.isMicrophoneMuted = isMuted + if callScreenState.isLocalAudioMuted != isMuted { + callScreenState.isLocalAudioMuted = isMuted self.callScreenState = callScreenState self.update(transition: .animated(duration: 0.3, curve: .spring)) } @@ -310,6 +322,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP case let .active(startTime, signalQuality, keyData): self.callStartTimestamp = startTime + var signalQuality = signalQuality + signalQuality = 4 + let _ = keyData mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( startTime: startTime + kCFAbsoluteTimeIntervalSince1970, @@ -320,7 +335,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP let _ = keyData mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( startTime: startTime + kCFAbsoluteTimeIntervalSince1970, - signalInfo: PrivateCallScreen.State.SignalInfo(quality: 0.0), + signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0), emojiKey: self.resolvedEmojiKey(data: keyData) )) case .terminating, .terminated: @@ -373,6 +388,13 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP callScreenState.isRemoteBatteryLow = false } + switch callState.remoteAudioState { + case .muted: + callScreenState.isRemoteAudioMuted = true + case .active: + callScreenState.isRemoteAudioMuted = false + } + if self.callScreenState != callScreenState { self.callScreenState = callScreenState self.update(transition: .animated(duration: 0.35, curve: .spring)) @@ -393,6 +415,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } callScreenState.name = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + callScreenState.shortName = peer.compactDisplayTitle if self.currentPeer?.smallProfileImage != peer.smallProfileImage { self.peerAvatarDisposable?.dispose() @@ -460,16 +483,14 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.containerView.layer.removeAnimation(forKey: "scale") self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - if !self.shouldStayHiddenUntilConnection { - self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3) - self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } + self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3) + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } func animateOut(completion: @escaping () -> Void) { self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - if !self.shouldStayHiddenUntilConnection || self.containerView.alpha > 0.0 { + if self.containerView.alpha > 0.0 { self.containerView.layer.allowsGroupOpacity = true self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in self?.containerView.layer.allowsGroupOpacity = false @@ -499,10 +520,18 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP transition.updateFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(view: self.callScreen, frame: CGRect(origin: CGPoint(), size: layout.size)) - if let callScreenState = self.callScreenState { + if var callScreenState = self.callScreenState { + if case .terminated = callScreenState.lifecycleState { + callScreenState.isLocalAudioMuted = false + callScreenState.isRemoteAudioMuted = false + callScreenState.isRemoteBatteryLow = false + callScreenState.localVideo = nil + callScreenState.remoteVideo = nil + } self.callScreen.update( size: layout.size, insets: layout.insets(options: [.statusBar]), + interfaceOrientation: layout.metrics.orientation ?? .portrait, screenCornerRadius: layout.deviceMetrics.screenCornerRadius, state: callScreenState, transition: Transition(transition) @@ -511,7 +540,100 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } } +private func copyI420BufferToNV12Buffer(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer, pixelBuffer: CVPixelBuffer) -> Bool { + guard CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange else { + return false + } + guard CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) == buffer.width else { + return false + } + guard CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) == buffer.height else { + return false + } + + let cvRet = CVPixelBufferLockBaseAddress(pixelBuffer, []) + if cvRet != kCVReturnSuccess { + return false + } + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, []) + } + + guard let dstY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { + return false + } + let dstStrideY = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0) + + guard let dstUV = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { + return false + } + let dstStrideUV = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1) + + buffer.y.withUnsafeBytes { srcYBuffer in + guard let srcY = srcYBuffer.baseAddress else { + return + } + buffer.u.withUnsafeBytes { srcUBuffer in + guard let srcU = srcUBuffer.baseAddress else { + return + } + buffer.v.withUnsafeBytes { srcVBuffer in + guard let srcV = srcVBuffer.baseAddress else { + return + } + libyuv_I420ToNV12( + srcY.assumingMemoryBound(to: UInt8.self), + Int32(buffer.strideY), + srcU.assumingMemoryBound(to: UInt8.self), + Int32(buffer.strideU), + srcV.assumingMemoryBound(to: UInt8.self), + Int32(buffer.strideV), + dstY.assumingMemoryBound(to: UInt8.self), + Int32(dstStrideY), + dstUV.assumingMemoryBound(to: UInt8.self), + Int32(dstStrideUV), + Int32(buffer.width), + Int32(buffer.height) + ) + } + } + } + + return true +} + private final class AdaptedCallVideoSource: VideoSource { + final class I420DataBuffer: Output.DataBuffer { + private let buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer + + override var pixelBuffer: CVPixelBuffer? { + let ioSurfaceProperties = NSMutableDictionary() + let options = NSMutableDictionary() + options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString) + + var pixelBuffer: CVPixelBuffer? + CVPixelBufferCreate( + kCFAllocatorDefault, + self.buffer.width, + self.buffer.height, + kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + options, + &pixelBuffer + ) + if let pixelBuffer, copyI420BufferToNV12Buffer(buffer: buffer, pixelBuffer: pixelBuffer) { + return pixelBuffer + } else { + return nil + } + } + + init(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer) { + self.buffer = buffer + + super.init() + } + } + private static let queue = Queue(name: "AdaptedCallVideoSource") private var onUpdatedListeners = Bag<() -> Void>() private(set) var currentOutput: Output? @@ -540,6 +662,8 @@ private final class AdaptedCallVideoSource: VideoSource { rotationAngle = Float.pi * 3.0 / 2.0 } + let followsDeviceOrientation = videoFrameData.deviceRelativeOrientation != nil + var mirrorDirection: Output.MirrorDirection = [] var sourceId: Int = 0 @@ -601,12 +725,45 @@ private final class AdaptedCallVideoSource: VideoSource { output = Output( resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)), - y: yTexture, - uv: uvTexture, + textureLayout: .biPlanar(Output.BiPlanarTextureLayout( + y: yTexture, + uv: uvTexture + )), + dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer), rotationAngle: rotationAngle, + followsDeviceOrientation: followsDeviceOrientation, mirrorDirection: mirrorDirection, sourceId: sourceId ) + case let .i420(i420Buffer): + let width = i420Buffer.width + let height = i420Buffer.height + + let _ = width + let _ = height + return + + /*var cvMetalTextureY: CVMetalTexture? + var status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .r8Unorm, width, height, 0, &cvMetalTextureY) + guard status == kCVReturnSuccess, let yTexture = CVMetalTextureGetTexture(cvMetalTextureY!) else { + return + } + var cvMetalTextureUV: CVMetalTexture? + status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .rg8Unorm, width / 2, height / 2, 1, &cvMetalTextureUV) + guard status == kCVReturnSuccess, let uvTexture = CVMetalTextureGetTexture(cvMetalTextureUV!) else { + return + } + + output = Output( + resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)), + y: yTexture, + uv: uvTexture, + dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer), + rotationAngle: rotationAngle, + followsDeviceOrientation: followsDeviceOrientation, + mirrorDirection: mirrorDirection, + sourceId: sourceId + )*/ default: return } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 031a9353b7..9218f305ea 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -342,6 +342,32 @@ public final class PresentationCallImpl: PresentationCall { } private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?, audioSessionControl: ManagedAudioSessionControl?) { + self.reception = reception + + if let ongoingContext = self.ongoingContext { + if self.receptionDisposable == nil, case .active = sessionState.state { + self.reception = 4 + + var canUpdate = false + self.receptionDisposable = (ongoingContext.reception + |> delay(1.0, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] reception in + if let strongSelf = self { + if let sessionState = strongSelf.sessionState { + if canUpdate { + strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl) + } else { + strongSelf.reception = reception + } + } else { + strongSelf.reception = reception + } + } + }) + canUpdate = true + } + } + if case .video = sessionState.type { self.isVideo = true } @@ -349,9 +375,10 @@ public final class PresentationCallImpl: PresentationCall { let previousControl = self.audioSessionControl self.sessionState = sessionState self.callContextState = callContextState - self.reception = reception self.audioSessionControl = audioSessionControl + let reception = self.reception + if previousControl != nil && audioSessionControl == nil { print("updateSessionState \(sessionState.state) \(audioSessionControl != nil)") } @@ -559,17 +586,6 @@ public final class PresentationCallImpl: PresentationCall { } }) - self.receptionDisposable = (ongoingContext.reception - |> deliverOnMainQueue).start(next: { [weak self] reception in - if let strongSelf = self { - if let sessionState = strongSelf.sessionState { - strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl) - } else { - strongSelf.reception = reception - } - } - }) - self.audioLevelDisposable = (ongoingContext.audioLevel |> deliverOnMainQueue).start(next: { [weak self] level in if let strongSelf = self { diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index 258494f0f3..2914559d88 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -514,7 +514,7 @@ func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializa } if useNetworkFramework { - if #available(iOS 12.0, macOS 10.14, *) { + if #available(iOS 12.0, macOS 14.0, *) { context.makeTcpConnectionInterface = { delegate, delegateQueue in return NetworkFrameworkTcpConnectionInterface(delegate: delegate, delegateQueue: delegateQueue) } diff --git a/submodules/TelegramCore/Sources/Network/NetworkFrameworkTcpConnectionInterface.swift b/submodules/TelegramCore/Sources/Network/NetworkFrameworkTcpConnectionInterface.swift index 956163880d..d17d399bb9 100644 --- a/submodules/TelegramCore/Sources/Network/NetworkFrameworkTcpConnectionInterface.swift +++ b/submodules/TelegramCore/Sources/Network/NetworkFrameworkTcpConnectionInterface.swift @@ -4,7 +4,7 @@ import Network import MtProtoKit import SwiftSignalKit -@available(iOS 12.0, macOS 10.14, *) +@available(iOS 12.0, macOS 14.0, *) final class NetworkFrameworkTcpConnectionInterface: NSObject, MTTcpConnectionInterface { private struct ReadRequest { let length: Int diff --git a/submodules/TelegramCore/Sources/WebpagePreview.swift b/submodules/TelegramCore/Sources/WebpagePreview.swift index 17e64c6a2d..21f7e46032 100644 --- a/submodules/TelegramCore/Sources/WebpagePreview.swift +++ b/submodules/TelegramCore/Sources/WebpagePreview.swift @@ -3,7 +3,11 @@ import Postbox import SwiftSignalKit import TelegramApi import MtProtoKit - +import LinkPresentation +#if os(iOS) +import UIKit +#endif +import CoreServices public enum WebpagePreviewResult: Equatable { public struct Result: Equatable { @@ -14,9 +18,13 @@ public enum WebpagePreviewResult: Equatable { case progress case result(Result?) } +#if os(macOS) +private typealias UIImage = NSImage +#endif -public func webpagePreview(account: Account, urls: [String], webpageId: MediaId? = nil) -> Signal { - return webpagePreviewWithProgress(account: account, urls: urls) + +public func webpagePreview(account: Account, urls: [String], webpageId: MediaId? = nil, forPeerId: PeerId? = nil) -> Signal { + return webpagePreviewWithProgress(account: account, urls: urls, webpageId: webpageId, forPeerId: forPeerId) |> mapToSignal { next -> Signal in if case let .result(result) = next { return .single(.result(result)) @@ -35,7 +43,7 @@ public func normalizedWebpagePreviewUrl(url: String) -> String { return url } -public func webpagePreviewWithProgress(account: Account, urls: [String], webpageId: MediaId? = nil) -> Signal { +public func webpagePreviewWithProgress(account: Account, urls: [String], webpageId: MediaId? = nil, forPeerId: PeerId? = nil) -> Signal { return account.postbox.transaction { transaction -> Signal in if let webpageId = webpageId, let webpage = transaction.getMedia(webpageId) as? TelegramMediaWebpage, let url = webpage.content.url { var sourceUrl = url @@ -44,6 +52,108 @@ public func webpagePreviewWithProgress(account: Account, urls: [String], webpage } return .single(.result(WebpagePreviewResult.Result(webpage: webpage, sourceUrl: sourceUrl))) } else { + if #available(iOS 13.0, macOS 10.15, *) { + if let forPeerId, forPeerId.namespace == Namespaces.Peer.SecretChat, let sourceUrl = urls.first, let url = URL(string: sourceUrl) { + let localHosts: [String] = [ + "twitter.com", + "www.twitter.com", + "instagram.com", + "www.instagram.com", + "tiktok.com", + "www.tiktok.com" + ] + if let host = url.host?.lowercased(), localHosts.contains(host) { + return Signal { subscriber in + subscriber.putNext(.progress(0.0)) + + let metadataProvider = LPMetadataProvider() + metadataProvider.shouldFetchSubresources = true + metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in + if let metadata = metadata { + let completeWithImage: (Data?) -> Void = { imageData in + var image: TelegramMediaImage? + if let imageData, let parsedImage = UIImage(data: imageData) { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + account.postbox.mediaBox.storeResourceData(resource.id, data: imageData) + image = TelegramMediaImage( + imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), + representations: [ + TelegramMediaImageRepresentation( + dimensions: PixelDimensions(width: Int32(parsedImage.size.width), height: Int32(parsedImage.size.height)), + resource: resource, + progressiveSizes: [], + immediateThumbnailData: nil, + hasVideo: false, + isPersonal: false + ) + ], + immediateThumbnailData: nil, + reference: nil, + partialReference: nil, + flags: [] + ) + } + + var webpageType: String? + if image != nil { + webpageType = "photo" + } + + let webpage = TelegramMediaWebpage( + webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), + content: .Loaded(TelegramMediaWebpageLoadedContent( + url: sourceUrl, + displayUrl: metadata.url?.absoluteString ?? sourceUrl, + hash: 0, + type: webpageType, + websiteName: nil, + title: metadata.title, + text: metadata.value(forKey: "_summary") as? String, + embedUrl: nil, + embedType: nil, + embedSize: nil, + duration: nil, + author: nil, + isMediaLargeByDefault: true, + image: image, + file: nil, + story: nil, + attributes: [], + instantPage: nil + )) + ) + subscriber.putNext(.result(WebpagePreviewResult.Result( + webpage: webpage, + sourceUrl: sourceUrl + ))) + subscriber.putCompletion() + } + + if let imageProvider = metadata.imageProvider { + imageProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeImage as String, completionHandler: { imageUrl, _ in + guard let imageUrl, let imageData = try? Data(contentsOf: imageUrl) else { + completeWithImage(nil) + return + } + completeWithImage(imageData) + }) + } else { + completeWithImage(nil) + } + } else { + subscriber.putNext(.result(nil)) + subscriber.putCompletion() + } + }) + + return ActionDisposable { + metadataProvider.cancel() + } + } + } + } + } + return account.network.requestWithAdditionalInfo(Api.functions.messages.getWebPagePreview(flags: 0, message: urls.joined(separator: " "), entities: nil), info: .progress) |> `catch` { _ -> Signal, NoError> in return .single(.result(.messageMediaEmpty)) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD index 6d202c1f72..ac35b7c0f2 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD +++ b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD @@ -67,6 +67,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/AppBundle", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal index d2bc62de18..1402203f29 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal @@ -236,7 +236,7 @@ fragment half4 callBlobFragment( return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha); } -kernel void videoYUVToRGBA( +kernel void videoBiPlanarToRGBA( texture2d inTextureY [[ texture(0) ]], texture2d inTextureUV [[ texture(1) ]], texture2d outTexture [[ texture(2) ]], @@ -249,6 +249,22 @@ kernel void videoYUVToRGBA( outTexture.write(color, threadPosition); } +kernel void videoTriPlanarToRGBA( + texture2d inTextureY [[ texture(0) ]], + texture2d inTextureU [[ texture(1) ]], + texture2d inTextureV [[ texture(2) ]], + texture2d outTexture [[ texture(3) ]], + uint2 threadPosition [[ thread_position_in_grid ]] +) { + half y = inTextureY.read(threadPosition).r; + uint2 uvPosition = uint2(threadPosition.x / 2, threadPosition.y / 2); + half2 inUV = (inTextureU.read(uvPosition).r, inTextureV.read(uvPosition).r); + half2 uv = inUV - half2(0.5, 0.5); + + half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0); + outTexture.write(color, threadPosition); +} + vertex QuadVertexOut mainVideoVertex( const device Rectangle &rect [[ buffer(0) ]], const device uint2 &mirror [[ buffer(1) ]], diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index c1a20242eb..0e86f8f821 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -58,8 +58,10 @@ final class ButtonGroupView: OverlayMaskContainerView { private var buttons: [Button]? private var buttonViews: [Button.Content.Key: ContentOverlayButton] = [:] - private var noticeViews: [AnyHashable: NoticeView] = [:] + private var closeButtonView: CloseButtonView? + + var closePressed: (() -> Void)? override init(frame: CGRect) { super.init(frame: frame) @@ -79,7 +81,7 @@ final class ButtonGroupView: OverlayMaskContainerView { return result } - func update(size: CGSize, insets: UIEdgeInsets, controlsHidden: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat { + func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat { self.buttons = buttons let buttonSize: CGFloat = 56.0 @@ -163,6 +165,46 @@ final class ButtonGroupView: OverlayMaskContainerView { } var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(buttons.count) - buttonSpacing * CGFloat(buttons.count - 1)) * 0.5) + if displayClose { + let closeButtonView: CloseButtonView + var closeButtonTransition = transition + var animateIn = false + if let current = self.closeButtonView { + closeButtonView = current + } else { + closeButtonTransition = closeButtonTransition.withAnimation(.none) + animateIn = true + + closeButtonView = CloseButtonView() + self.closeButtonView = closeButtonView + self.addSubview(closeButtonView) + closeButtonView.pressAction = { [weak self] in + guard let self else { + return + } + self.closePressed?() + } + } + let closeButtonSize = CGSize(width: minWidth, height: buttonSize) + closeButtonView.update(text: "Close", size: closeButtonSize, transition: closeButtonTransition) + closeButtonTransition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((size.width - closeButtonSize.width) * 0.5), y: buttonY), size: closeButtonSize)) + + if animateIn && !transition.animation.isImmediate { + closeButtonView.animateIn() + } + } else { + if let closeButtonView = self.closeButtonView { + self.closeButtonView = nil + if !transition.animation.isImmediate { + closeButtonView.animateOut(completion: { [weak closeButtonView] in + closeButtonView?.removeFromSuperview() + }) + } else { + closeButtonView.removeFromSuperview() + } + } + } + for button in buttons { let title: String let image: UIImage? @@ -213,9 +255,10 @@ final class ButtonGroupView: OverlayMaskContainerView { Transition.immediate.setScale(view: buttonView, scale: 0.001) buttonView.alpha = 0.0 transition.setScale(view: buttonView, scale: 1.0) - transition.setAlpha(view: buttonView, alpha: 1.0) } + transition.setAlpha(view: buttonView, alpha: displayClose ? 0.0 : 1.0) + buttonTransition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize))) buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: buttonTransition) buttonX += buttonSize + buttonSpacing diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift new file mode 100644 index 0000000000..110814ead6 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift @@ -0,0 +1,185 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import UIKitRuntimeUtils + +final class CloseButtonView: HighlightTrackingButton, OverlayMaskContainerViewProtocol { + private struct Params: Equatable { + var text: String + var size: CGSize + + init(text: String, size: CGSize) { + self.text = text + self.size = size + } + } + + private let backdropBackgroundView: RoundedCornersView + private let backgroundView: RoundedCornersView + private let backgroundMaskView: UIView + private let backgroundClippingView: UIView + + private let duration: Double = 5.0 + private var fillTime: Double = 0.0 + + private let backgroundTextView: TextView + private let backgroundTextClippingView: UIView + + private let textView: TextView + + var pressAction: (() -> Void)? + + private var params: Params? + private var updateDisplayLink: SharedDisplayLinkDriver.Link? + + let maskContents: UIView + override static var layerClass: AnyClass { + return MirroringLayer.self + } + + override init(frame: CGRect) { + self.backdropBackgroundView = RoundedCornersView(color: .white, smoothCorners: true) + self.backdropBackgroundView.update(cornerRadius: 12.0, transition: .immediate) + + self.backgroundView = RoundedCornersView(color: .white, smoothCorners: true) + self.backgroundView.update(cornerRadius: 12.0, transition: .immediate) + self.backgroundView.isUserInteractionEnabled = false + + self.backgroundMaskView = UIView() + self.backgroundMaskView.backgroundColor = .white + self.backgroundView.mask = self.backgroundMaskView + if let filter = makeLuminanceToAlphaFilter() { + self.backgroundMaskView.layer.filters = [filter] + } + + self.backgroundClippingView = UIView() + self.backgroundClippingView.clipsToBounds = true + self.backgroundClippingView.layer.cornerRadius = 12.0 + + self.backgroundTextClippingView = UIView() + self.backgroundTextClippingView.clipsToBounds = true + + self.backgroundTextView = TextView() + self.textView = TextView() + + self.maskContents = UIView() + + self.maskContents.addSubview(self.backdropBackgroundView) + + super.init(frame: frame) + + (self.layer as? MirroringLayer)?.targetLayer = self.maskContents.layer + + self.backgroundTextClippingView.addSubview(self.backgroundTextView) + self.backgroundTextClippingView.isUserInteractionEnabled = false + self.addSubview(self.backgroundTextClippingView) + + self.backgroundClippingView.addSubview(self.backgroundView) + self.backgroundClippingView.isUserInteractionEnabled = false + self.addSubview(self.backgroundClippingView) + + self.backgroundMaskView.addSubview(self.textView) + + self.internalHighligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "sublayerTransform") + let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) + transition.setScale(layer: self.layer, scale: topScale) + } else { + let t = self.layer.presentation()?.transform ?? layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + + let transition = Transition(animation: .none) + transition.setScale(layer: self.layer, scale: 1.0) + + self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in + guard let self, completed else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + (self.layer as? MirroringLayer)?.didEnterHierarchy = { [weak self] in + guard let self else { + return + } + if self.fillTime < self.duration && self.updateDisplayLink == nil { + self.updateDisplayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + guard let self else { + return + } + self.fillTime = min(self.duration, self.fillTime + deltaTime) + if let params = self.params { + self.update(params: params, transition: .immediate) + } + + if self.fillTime >= self.duration { + self.updateDisplayLink = nil + } + }) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.pressAction?() + } + + func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func update(text: String, size: CGSize, transition: Transition) { + let params = Params(text: text, size: size) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + private func update(params: Params, transition: Transition) { + let fillFraction: CGFloat = CGFloat(self.fillTime / self.duration) + + let sideInset: CGFloat = 12.0 + let textSize = self.textView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.semibold.rawValue, color: .black, constrainedWidth: params.size.width - sideInset * 2.0, transition: .immediate) + let _ = self.backgroundTextView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.semibold.rawValue, color: .white, constrainedWidth: params.size.width - sideInset * 2.0, transition: .immediate) + + transition.setFrame(view: self.backdropBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: params.size)) + + let progressWidth: CGFloat = max(0.0, min(params.size.width, floorToScreenPixels(fillFraction * params.size.width))) + let backgroundClippingFrame = CGRect(origin: CGPoint(x: progressWidth, y: 0.0), size: CGSize(width: params.size.width - progressWidth, height: params.size.height)) + transition.setPosition(view: self.backgroundClippingView, position: backgroundClippingFrame.center) + transition.setBounds(view: self.backgroundClippingView, bounds: CGRect(origin: CGPoint(x: backgroundClippingFrame.minX, y: 0.0), size: backgroundClippingFrame.size)) + + let backgroundTextClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: progressWidth, height: params.size.height)) + transition.setPosition(view: self.backgroundTextClippingView, position: backgroundTextClippingFrame.center) + transition.setBounds(view: self.backgroundTextClippingView, bounds: CGRect(origin: CGPoint(), size: backgroundTextClippingFrame.size)) + + let textFrame = CGRect(origin: CGPoint(x: floor((params.size.width - textSize.width) * 0.5), y: floor((params.size.height - textSize.height) * 0.5)), size: textSize) + transition.setFrame(view: self.textView, frame: textFrame) + transition.setFrame(view: self.backgroundTextView, frame: textFrame) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift index 12d62e5104..e02f4c9d00 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift @@ -63,8 +63,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width if highlighted { - self.layer.removeAnimation(forKey: "opacity") - self.layer.removeAnimation(forKey: "transform") + self.layer.removeAnimation(forKey: "sublayerTransform") let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) transition.setScale(layer: self.layer, scale: topScale) } else { diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift index 3a0cf981bd..f2b0ffd7ec 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift @@ -5,10 +5,10 @@ import ComponentFlow final class EmojiExpandedInfoView: OverlayMaskContainerView { private struct Params: Equatable { - var constrainedWidth: CGFloat + var width: CGFloat - init(constrainedWidth: CGFloat) { - self.constrainedWidth = constrainedWidth + init(width: CGFloat) { + self.width = width } } @@ -129,8 +129,8 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView { return nil } - func update(constrainedWidth: CGFloat, transition: Transition) -> CGSize { - let params = Params(constrainedWidth: constrainedWidth) + func update(width: CGFloat, transition: Transition) -> CGSize { + let params = Params(width: width) if let currentLayout = self.currentLayout, currentLayout.params == params { return currentLayout.size } @@ -142,16 +142,12 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView { private func update(params: Params, transition: Transition) -> CGSize { let buttonHeight: CGFloat = 56.0 - var constrainedWidth = params.constrainedWidth - constrainedWidth = min(constrainedWidth, 300.0) + let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition) + let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition) - let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition) - let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition) - - let contentWidth: CGFloat = max(titleSize.width, textSize.width) + 26.0 * 2.0 let contentHeight = 78.0 + titleSize.height + 10.0 + textSize.height + 22.0 + buttonHeight - let size = CGSize(width: contentWidth, height: contentHeight) + let size = CGSize(width: params.width, height: contentHeight) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift new file mode 100644 index 0000000000..b5a52fd67f --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift @@ -0,0 +1,248 @@ +import Foundation +import AVKit +import AVFoundation +import CoreMedia +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit + +private func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? { + var maybeFormat: CMVideoFormatDescription? + let status = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &maybeFormat) + if status != noErr { + return nil + } + guard let format = maybeFormat else { + return nil + } + + var timingInfo = CMSampleTimingInfo( + duration: CMTimeMake(value: 1, timescale: 30), + presentationTimeStamp: CMTimeMake(value: 0, timescale: 30), + decodeTimeStamp: CMTimeMake(value: 0, timescale: 30) + ) + + var maybeSampleBuffer: CMSampleBuffer? + let bufferStatus = CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &maybeSampleBuffer) + + if (bufferStatus != noErr) { + return nil + } + guard let sampleBuffer = maybeSampleBuffer else { + return nil + } + + let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true)! as NSArray + let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary + dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber + + return sampleBuffer +} + +final class PrivateCallPictureInPictureView: UIView { + private final class SampleBufferView: UIView { + override static var layerClass: AnyClass { + return AVSampleBufferDisplayLayer.self + } + } + + private final class AnimationTrackingLayer: SimpleLayer { + var onAnimation: ((CAAnimation) -> Void)? + + override func add(_ anim: CAAnimation, forKey key: String?) { + super.add(anim, forKey: key) + + if key == "bounds" { + self.onAnimation?(anim) + } + } + } + + private final class AnimationTrackingView: UIView { + override static var layerClass: AnyClass { + return AnimationTrackingLayer.self + } + + var onAnimation: ((CAAnimation) -> Void)? { + didSet { + (self.layer as? AnimationTrackingLayer)?.onAnimation = self.onAnimation + } + } + } + + private let animationTrackingView: AnimationTrackingView + + private let videoContainerView: UIView + private let sampleBufferView: SampleBufferView + + private var videoMetrics: VideoContainerView.VideoMetrics? + private var videoDisposable: Disposable? + + var isRenderingEnabled: Bool = false { + didSet { + if self.isRenderingEnabled != oldValue { + self.updateContents() + } + } + } + var video: VideoSource? { + didSet { + if self.video !== oldValue { + self.videoDisposable?.dispose() + if let video = self.video { + self.videoDisposable = video.addOnUpdated({ [weak self] in + guard let self else { + return + } + if self.isRenderingEnabled { + self.updateContents() + } + }) + } + } + } + } + + override static var layerClass: AnyClass { + return AVSampleBufferDisplayLayer.self + } + + override init(frame: CGRect) { + self.animationTrackingView = AnimationTrackingView() + + self.videoContainerView = UIView() + self.sampleBufferView = SampleBufferView() + + super.init(frame: frame) + + self.addSubview(self.animationTrackingView) + + self.backgroundColor = .black + + self.videoContainerView.addSubview(self.sampleBufferView) + self.addSubview(self.videoContainerView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateContents() { + guard let video = self.video, let currentOutput = video.currentOutput else { + return + } + guard let pixelBuffer = currentOutput.dataBuffer.pixelBuffer else { + return + } + let videoMetrics = VideoContainerView.VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, followsDeviceOrientation: currentOutput.followsDeviceOrientation, sourceId: currentOutput.sourceId) + if self.videoMetrics != videoMetrics { + self.videoMetrics = videoMetrics + self.setNeedsLayout() + } + + if let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: pixelBuffer) { + (self.sampleBufferView.layer as? AVSampleBufferDisplayLayer)?.enqueue(sampleBuffer) + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + let size = self.bounds.size + if size.width.isZero || size.height.isZero { + return + } + + var animationTemplate: CAAnimation? + self.animationTrackingView.onAnimation = { animation in + animationTemplate = animation + } + self.animationTrackingView.frame = CGRect(origin: CGPoint(), size: size) + self.animationTrackingView.onAnimation = nil + + let _ = animationTemplate + + let animationDuration = CATransaction.animationDuration() + let timingFunction = CATransaction.animationTimingFunction() + + let mappedTransition: Transition + if self.sampleBufferView.bounds.isEmpty { + mappedTransition = .immediate + } else if animationDuration > 0.0 && !CATransaction.disableActions() { + let mappedCurve: Transition.Animation.Curve + if let timingFunction { + var controlPoint0: [Float] = [0.0, 0.0] + var controlPoint1: [Float] = [0.0, 0.0] + timingFunction.getControlPoint(at: 1, values: &controlPoint0) + timingFunction.getControlPoint(at: 2, values: &controlPoint1) + mappedCurve = .custom(controlPoint0[0], controlPoint0[1], controlPoint1[0], controlPoint1[1]) + } else if animationDuration >= 0.5 { + mappedCurve = .spring + } else { + mappedCurve = .easeInOut + } + mappedTransition = Transition(animation: .curve( + duration: animationDuration, + curve: mappedCurve + )) + } else { + mappedTransition = .immediate + } + + if let videoMetrics = self.videoMetrics { + let resolvedRotationAngle = resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: UIApplication.shared.statusBarOrientation) + + var rotatedResolution = videoMetrics.resolution + var videoIsRotated = false + if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 { + rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) + videoIsRotated = true + } + + var videoSize = rotatedResolution.aspectFitted(size) + let boundingAspectRatio = size.width / size.height + let videoAspectRatio = videoSize.width / videoSize.height + let isFillingBounds = abs(boundingAspectRatio - videoAspectRatio) < 0.15 + if isFillingBounds { + videoSize = rotatedResolution.aspectFilled(size) + } + + let rotatedBoundingSize = videoIsRotated ? CGSize(width: size.height, height: size.width) : size + let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize + + let videoFrame = rotatedVideoSize.centered(around: CGPoint(x: rotatedBoundingSize.width * 0.5, y: rotatedBoundingSize.height * 0.5)) + + let apply: () -> Void = { + self.videoContainerView.center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) + self.videoContainerView.bounds = CGRect(origin: CGPoint(), size: rotatedBoundingSize) + self.videoContainerView.transform = CGAffineTransformMakeRotation(CGFloat(resolvedRotationAngle)) + + self.sampleBufferView.center = videoFrame.center + self.sampleBufferView.bounds = CGRect(origin: CGPoint(), size: videoFrame.size) + + if let sublayers = self.sampleBufferView.layer.sublayers { + if sublayers.count > 1, !sublayers[0].bounds.isEmpty { + sublayers[0].position = CGPoint(x: videoFrame.width * 0.5, y: videoFrame.height * 0.5) + sublayers[0].bounds = CGRect(origin: CGPoint(), size: videoFrame.size) + } + } + } + + if !mappedTransition.animation.isImmediate { + apply() + } else { + UIView.performWithoutAnimation { + apply() + } + } + } + } +} + +@available(iOS 15.0, *) +final class PrivateCallPictureInPictureController: AVPictureInPictureVideoCallViewController { + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift index 53ada01185..ee5b999890 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift @@ -11,7 +11,8 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { let blurredLayer: MetalEngineSubjectLayer final class BlurState: ComputeState { - let computePipelineStateYUVToRGBA: MTLComputePipelineState + let computePipelineStateYUVBiPlanarToRGBA: MTLComputePipelineState + let computePipelineStateYUVTriPlanarToRGBA: MTLComputePipelineState let computePipelineStateHorizontal: MTLComputePipelineState let computePipelineStateVertical: MTLComputePipelineState let downscaleKernel: MPSImageBilinearScale @@ -20,13 +21,22 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { guard let library = metalLibrary(device: device) else { return nil } - guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else { + + guard let functionVideoBiPlanarToRGBA = library.makeFunction(name: "videoBiPlanarToRGBA") else { return nil } - guard let computePipelineStateYUVToRGBA = try? device.makeComputePipelineState(function: functionVideoYUVToRGBA) else { + guard let computePipelineStateYUVBiPlanarToRGBA = try? device.makeComputePipelineState(function: functionVideoBiPlanarToRGBA) else { return nil } - self.computePipelineStateYUVToRGBA = computePipelineStateYUVToRGBA + self.computePipelineStateYUVBiPlanarToRGBA = computePipelineStateYUVBiPlanarToRGBA + + guard let functionVideoTriPlanarToRGBA = library.makeFunction(name: "videoTriPlanarToRGBA") else { + return nil + } + guard let computePipelineStateYUVTriPlanarToRGBA = try? device.makeComputePipelineState(function: functionVideoTriPlanarToRGBA) else { + return nil + } + self.computePipelineStateYUVTriPlanarToRGBA = computePipelineStateYUVTriPlanarToRGBA guard let gaussianBlurHorizontal = library.makeFunction(name: "gaussianBlurHorizontal"), let gaussianBlurVertical = library.makeFunction(name: "gaussianBlurVertical") else { return nil @@ -107,7 +117,7 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { return } - let rgbaTextureSpec = TextureSpec(width: videoTextures.y.width, height: videoTextures.y.height, pixelFormat: .rgba8UnsignedNormalized) + let rgbaTextureSpec = TextureSpec(width: Int(videoTextures.resolution.width), height: Int(videoTextures.resolution.height), pixelFormat: .rgba8UnsignedNormalized) if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec { self.rgbaTexture = MetalEngine.shared.pooledTexture(spec: rgbaTextureSpec) } @@ -136,10 +146,19 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1) let threadgroupCount = MTLSize(width: (rgbaTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (rgbaTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1) - computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVToRGBA) - computeEncoder.setTexture(videoTextures.y, index: 0) - computeEncoder.setTexture(videoTextures.uv, index: 1) - computeEncoder.setTexture(rgbaTexture, index: 2) + switch videoTextures.textureLayout { + case let .biPlanar(biPlanar): + computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVBiPlanarToRGBA) + computeEncoder.setTexture(biPlanar.y, index: 0) + computeEncoder.setTexture(biPlanar.uv, index: 1) + computeEncoder.setTexture(rgbaTexture, index: 2) + case let .triPlanar(triPlanar): + computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVTriPlanarToRGBA) + computeEncoder.setTexture(triPlanar.y, index: 0) + computeEncoder.setTexture(triPlanar.u, index: 1) + computeEncoder.setTexture(triPlanar.u, index: 2) + computeEncoder.setTexture(rgbaTexture, index: 3) + } computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) computeEncoder.endEncoding() @@ -198,8 +217,8 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { encoder.setFragmentTexture(blurredTexture, index: 0) - var brightness: Float = 1.0 - var saturation: Float = 1.2 + var brightness: Float = 0.7 + var saturation: Float = 1.3 var overlay: SIMD4 = SIMD4(1.0, 1.0, 1.0, 0.2) encoder.setFragmentBytes(&brightness, length: 4, index: 0) encoder.setFragmentBytes(&saturation, length: 4, index: 1) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift new file mode 100644 index 0000000000..6b54934313 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift @@ -0,0 +1,73 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +final class RatingView: OverlayMaskContainerView { + private let backgroundView: RoundedCornersView + private let textContainer: UIView + private let textView: TextView + + override init(frame: CGRect) { + self.backgroundView = RoundedCornersView(color: .white) + self.textContainer = UIView() + self.textContainer.clipsToBounds = true + self.textView = TextView() + + super.init(frame: frame) + + self.clipsToBounds = true + + self.maskContents.addSubview(self.backgroundView) + + self.textContainer.addSubview(self.textView) + self.addSubview(self.textContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func animateIn() { + let delay: Double = 0.2 + + self.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay) + + self.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.backgroundView.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.backgroundView.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) + + self.textContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + self.textContainer.layer.cornerRadius = self.bounds.height * 0.5 + self.textContainer.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.textContainer.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] completed in + guard let self, completed else { + return + } + self.textContainer.layer.cornerRadius = 0.0 + }) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + self.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + } + + func update(text: String, constrainedWidth: CGFloat, transition: Transition) -> CGSize { + let sideInset: CGFloat = 12.0 + let verticalInset: CGFloat = 6.0 + + let textSize = self.textView.update(string: text, fontSize: 15.0, fontWeight: 0.0, color: .white, constrainedWidth: constrainedWidth - sideInset * 2.0, transition: .immediate) + let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(cornerRadius: floor(size.height * 0.5), transition: transition) + + transition.setFrame(view: self.textContainer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(view: self.textView, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize)) + + return size + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift index 664616101a..af64a8b1a0 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift @@ -5,17 +5,16 @@ import ComponentFlow final class RoundedCornersView: UIImageView { private let color: UIColor + private let smoothCorners: Bool + private var currentCornerRadius: CGFloat? private var cornerImage: UIImage? - init(color: UIColor) { + init(color: UIColor, smoothCorners: Bool = false) { self.color = color + self.smoothCorners = smoothCorners super.init(image: nil) - - if #available(iOS 13.0, *) { - self.layer.cornerCurve = .circular - } } required init?(coder: NSCoder) { @@ -26,10 +25,33 @@ final class RoundedCornersView: UIImageView { guard let cornerRadius = self.currentCornerRadius else { return } - if let cornerImage = self.cornerImage, cornerImage.size.height == cornerRadius * 2.0 { + if cornerRadius == 0.0 { + if let cornerImage = self.cornerImage, cornerImage.size.width == 1.0 { + } else { + self.cornerImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.setFillColor(self.color.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + })?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5) + } } else { - let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0) - self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color) + if self.smoothCorners { + let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0) + if let cornerImage = self.cornerImage, cornerImage.size == size { + } else { + self.cornerImage = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath) + context.setFillColor(self.color.cgColor) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5) + } + } else { + let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0) + if let cornerImage = self.cornerImage, cornerImage.size == size { + } else { + self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color) + } + } } self.image = self.cornerImage self.clipsToBounds = false @@ -52,6 +74,14 @@ final class RoundedCornersView: UIImageView { if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil { self.layer.cornerRadius = previousCornerRadius } + if #available(iOS 13.0, *) { + if self.smoothCorners { + self.layer.cornerCurve = .continuous + } else { + self.layer.cornerCurve = .circular + } + + } transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in guard let self, completed else { return diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift index 6d321a3a37..46429e2aeb 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift @@ -9,6 +9,26 @@ private let shadowImage: UIImage? = { UIImage(named: "Call/VideoGradient")?.precomposed() }() +func resolveVideoRotationAngle(angle: Float, followsDeviceOrientation: Bool, interfaceOrientation: UIInterfaceOrientation) -> Float { + if !followsDeviceOrientation { + return angle + } + let interfaceAngle: Float + switch interfaceOrientation { + case .portrait, .unknown: + interfaceAngle = 0.0 + case .landscapeLeft: + interfaceAngle = Float.pi * 0.5 + case .landscapeRight: + interfaceAngle = Float.pi * 3.0 / 2.0 + case .portraitUpsideDown: + interfaceAngle = Float.pi + @unknown default: + interfaceAngle = 0.0 + } + return (angle + interfaceAngle).truncatingRemainder(dividingBy: Float.pi * 2.0) +} + private final class VideoContainerLayer: SimpleLayer { let contentsLayer: SimpleLayer @@ -44,14 +64,16 @@ final class VideoContainerView: HighlightTrackingButton { private struct Params: Equatable { var size: CGSize var insets: UIEdgeInsets + var interfaceOrientation: UIInterfaceOrientation var cornerRadius: CGFloat var controlsHidden: Bool var isMinimized: Bool var isAnimatedOut: Bool - init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool) { + init(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool) { self.size = size self.insets = insets + self.interfaceOrientation = interfaceOrientation self.cornerRadius = cornerRadius self.controlsHidden = controlsHidden self.isMinimized = isMinimized @@ -59,14 +81,16 @@ final class VideoContainerView: HighlightTrackingButton { } } - private struct VideoMetrics: Equatable { + struct VideoMetrics: Equatable { var resolution: CGSize var rotationAngle: Float + var followsDeviceOrientation: Bool var sourceId: Int - init(resolution: CGSize, rotationAngle: Float, sourceId: Int) { + init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool, sourceId: Int) { self.resolution = resolution self.rotationAngle = rotationAngle + self.followsDeviceOrientation = followsDeviceOrientation self.sourceId = sourceId } } @@ -74,10 +98,12 @@ final class VideoContainerView: HighlightTrackingButton { private final class FlipAnimationInfo { let isForward: Bool let previousRotationAngle: Float + let followsDeviceOrientation: Bool - init(isForward: Bool, previousRotationAngle: Float) { + init(isForward: Bool, previousRotationAngle: Float, followsDeviceOrientation: Bool) { self.isForward = isForward self.previousRotationAngle = previousRotationAngle + self.followsDeviceOrientation = followsDeviceOrientation } } @@ -141,11 +167,11 @@ final class VideoContainerView: HighlightTrackingButton { var videoMetrics: VideoMetrics? if let currentOutput = self.video?.currentOutput { if let previousVideo = self.videoLayer.video, previousVideo.sourceId != currentOutput.sourceId { - self.initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo(isForward: previousVideo.sourceId < currentOutput.sourceId, previousRotationAngle: previousVideo.rotationAngle)) + self.initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo(isForward: previousVideo.sourceId < currentOutput.sourceId, previousRotationAngle: previousVideo.rotationAngle, followsDeviceOrientation: previousVideo.followsDeviceOrientation)) } self.videoLayer.video = currentOutput - videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId) + videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, followsDeviceOrientation: currentOutput.followsDeviceOrientation, sourceId: currentOutput.sourceId) } else { self.videoLayer.video = nil } @@ -164,7 +190,7 @@ final class VideoContainerView: HighlightTrackingButton { var videoMetrics: VideoMetrics? if let currentOutput = self.video?.currentOutput { self.videoLayer.video = currentOutput - videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId) + videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, followsDeviceOrientation: currentOutput.followsDeviceOrientation, sourceId: currentOutput.sourceId) } else { self.videoLayer.video = nil } @@ -382,7 +408,7 @@ final class VideoContainerView: HighlightTrackingButton { self.dragPositionAnimatorLink = nil return } - let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: false) + let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, resolvedRotationAngle: resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: false) let targetPosition = videoLayout.rotatedVideoFrame.center self.dragVelocity = self.updateVelocityUsingSpring( @@ -443,8 +469,8 @@ final class VideoContainerView: HighlightTrackingButton { self.update(previousParams: params, params: params, transition: transition) } - func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) { - let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut) + func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) { + let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut) if self.params == params { return } @@ -469,10 +495,10 @@ final class VideoContainerView: HighlightTrackingButton { var effectiveVideoFrame: CGRect } - private func calculateMinimizedLayout(params: Params, videoMetrics: VideoMetrics, applyDragPosition: Bool) -> MinimizedLayout { + private func calculateMinimizedLayout(params: Params, videoMetrics: VideoMetrics, resolvedRotationAngle: Float, applyDragPosition: Bool) -> MinimizedLayout { var rotatedResolution = videoMetrics.resolution var videoIsRotated = false - if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 { + if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 { rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) videoIsRotated = true } @@ -505,7 +531,7 @@ final class VideoContainerView: HighlightTrackingButton { var videoTransform = CATransform3DIdentity videoTransform.m34 = 1.0 / 600.0 - videoTransform = CATransform3DRotate(videoTransform, CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0) + videoTransform = CATransform3DRotate(videoTransform, CGFloat(resolvedRotationAngle), 0.0, 0.0, 1.0) if params.isAnimatedOut { videoTransform = CATransform3DScale(videoTransform, 0.6, 0.6, 1.0) } @@ -530,10 +556,12 @@ final class VideoContainerView: HighlightTrackingButton { } self.appliedVideoMetrics = videoMetrics + let resolvedRotationAngle = resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation) + if params.isMinimized { self.isFillingBounds = false - let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: true) + let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, resolvedRotationAngle: resolvedRotationAngle, applyDragPosition: true) transition.setPosition(layer: self.videoContainerLayer, position: videoLayout.rotatedVideoFrame.center) @@ -558,23 +586,25 @@ final class VideoContainerView: HighlightTrackingButton { if let disappearingVideoLayer = self.disappearingVideoLayer { self.disappearingVideoLayer = nil - let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, applyDragPosition: true) + let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, resolvedRotationAngle: resolveVideoRotationAngle(angle: disappearingVideoLayer.videoMetrics.rotationAngle, followsDeviceOrientation: disappearingVideoLayer.videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: true) let initialDisapparingVideoSize = disappearingVideoLayout.rotatedVideoSize if !disappearingVideoLayer.isAlphaAnimationInitiated { disappearingVideoLayer.isAlphaAnimationInitiated = true if let flipAnimationInfo = disappearingVideoLayer.flipAnimationInfo { + let resolvedPreviousRotationAngle = resolveVideoRotationAngle(angle: flipAnimationInfo.previousRotationAngle, followsDeviceOrientation: flipAnimationInfo.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation) + var videoTransform = self.videoContainerLayer.transform var axis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0.0, 0.0, 0.0) let previousVideoScale: CGPoint - if flipAnimationInfo.previousRotationAngle == Float.pi * 0.5 { + if resolvedPreviousRotationAngle == Float.pi * 0.5 { axis.x = -1.0 previousVideoScale = CGPoint(x: 1.0, y: -1.0) - } else if flipAnimationInfo.previousRotationAngle == Float.pi { + } else if resolvedPreviousRotationAngle == Float.pi { axis.y = -1.0 previousVideoScale = CGPoint(x: -1.0, y: -1.0) - } else if flipAnimationInfo.previousRotationAngle == Float.pi * 3.0 / 2.0 { + } else if resolvedPreviousRotationAngle == Float.pi * 3.0 / 2.0 { axis.x = 1.0 previousVideoScale = CGPoint(x: 1.0, y: 1.0) } else { @@ -652,7 +682,7 @@ final class VideoContainerView: HighlightTrackingButton { } else { var rotatedResolution = videoMetrics.resolution var videoIsRotated = false - if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 { + if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 { rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) videoIsRotated = true } @@ -728,13 +758,13 @@ final class VideoContainerView: HighlightTrackingButton { } } - transition.setTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) + transition.setTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeRotation(CGFloat(resolvedRotationAngle), 0.0, 0.0, 1.0)) videoTransition.setFrame(layer: self.videoLayer, frame: rotatedVideoSize.centered(around: CGPoint(x: rotatedBoundingSize.width * 0.5, y: rotatedBoundingSize.height * 0.5))) videoTransition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center) videoTransition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size)) videoTransition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 1.0) - videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) + videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(resolvedRotationAngle), 0.0, 0.0, 1.0)) if !params.isAnimatedOut { self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift index 68977a2a5c..a0805cbe01 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift @@ -16,18 +16,67 @@ public final class VideoSourceOutput { public static let vertical = MirrorDirection(rawValue: 1 << 1) } + open class DataBuffer { + open var pixelBuffer: CVPixelBuffer? { + return nil + } + + public init() { + } + } + + public final class BiPlanarTextureLayout { + public let y: MTLTexture + public let uv: MTLTexture + + public init(y: MTLTexture, uv: MTLTexture) { + self.y = y + self.uv = uv + } + } + + public final class TriPlanarTextureLayout { + public let y: MTLTexture + public let u: MTLTexture + public let v: MTLTexture + + public init(y: MTLTexture, u: MTLTexture, v: MTLTexture) { + self.y = y + self.u = u + self.v = v + } + } + + public enum TextureLayout { + case biPlanar(BiPlanarTextureLayout) + case triPlanar(TriPlanarTextureLayout) + } + + public final class NativeDataBuffer: DataBuffer { + private let pixelBufferValue: CVPixelBuffer + override public var pixelBuffer: CVPixelBuffer? { + return self.pixelBufferValue + } + + public init(pixelBuffer: CVPixelBuffer) { + self.pixelBufferValue = pixelBuffer + } + } + public let resolution: CGSize - public let y: MTLTexture - public let uv: MTLTexture + public let textureLayout: TextureLayout + public let dataBuffer: DataBuffer public let rotationAngle: Float + public let followsDeviceOrientation: Bool public let mirrorDirection: MirrorDirection public let sourceId: Int - public init(resolution: CGSize, y: MTLTexture, uv: MTLTexture, rotationAngle: Float, mirrorDirection: MirrorDirection, sourceId: Int) { + public init(resolution: CGSize, textureLayout: TextureLayout, dataBuffer: DataBuffer, rotationAngle: Float, followsDeviceOrientation: Bool, mirrorDirection: MirrorDirection, sourceId: Int) { self.resolution = resolution - self.y = y - self.uv = uv + self.textureLayout = textureLayout + self.dataBuffer = dataBuffer self.rotationAngle = rotationAngle + self.followsDeviceOrientation = followsDeviceOrientation self.mirrorDirection = mirrorDirection self.sourceId = sourceId } @@ -161,7 +210,18 @@ public final class FileVideoSource: VideoSource { resolution.width = floor(resolution.width * self.sizeMultiplicator.x) resolution.height = floor(resolution.height * self.sizeMultiplicator.y) - self.currentOutput = Output(resolution: resolution, y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, mirrorDirection: [], sourceId: self.sourceId) + self.currentOutput = Output( + resolution: resolution, + textureLayout: .biPlanar(Output.BiPlanarTextureLayout( + y: yTexture, + uv: uvTexture + )), + dataBuffer: Output.NativeDataBuffer(pixelBuffer: buffer), + rotationAngle: rotationAngle, + followsDeviceOrientation: false, + mirrorDirection: [], + sourceId: self.sourceId + ) return true } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index b67bebc28c..38dac04c1e 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -1,58 +1,14 @@ import Foundation +import AVFoundation +import AVKit import UIKit import Display import MetalEngine import ComponentFlow import SwiftSignalKit +import UIKitRuntimeUtils -/*private final class EdgeTestLayer: MetalEngineSubjectLayer, MetalEngineSubject { - final class RenderState: RenderToLayerState { - let pipelineState: MTLRenderPipelineState - - required init?(device: MTLDevice) { - guard let library = metalLibrary(device: device) else { - return nil - } - guard let vertexFunction = library.makeFunction(name: "edgeTestVertex"), let fragmentFunction = library.makeFunction(name: "edgeTestFragment") else { - return nil - } - - let pipelineDescriptor = MTLRenderPipelineDescriptor() - pipelineDescriptor.vertexFunction = vertexFunction - pipelineDescriptor.fragmentFunction = fragmentFunction - pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm - pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true - pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add - pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add - pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one - pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one - pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha - pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one - - guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else { - return nil - } - self.pipelineState = pipelineState - } - } - - var internalData: MetalEngineSubjectInternalData? - - func update(context: MetalEngineSubjectContext) { - context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: 300, height: 300), edgeInset: 100), state: RenderState.self, layer: self, commands: { encoder, placement in - let effectiveRect = placement.effectiveRect - - var rect = SIMD4(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width * 0.5), Float(effectiveRect.height)) - encoder.setVertexBytes(&rect, length: 4 * 4, index: 0) - - var color = SIMD4(1.0, 0.0, 0.0, 1.0) - encoder.setFragmentBytes(&color, length: 4 * 4, index: 0) - encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) - }) - } -}*/ - -public final class PrivateCallScreen: OverlayMaskContainerView { +public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictureControllerDelegate { public struct State: Equatable { public struct SignalInfo: Equatable { public var quality: Double @@ -100,7 +56,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { public var shortName: String public var avatarImage: UIImage? public var audioOutput: AudioOutput - public var isMicrophoneMuted: Bool + public var isLocalAudioMuted: Bool + public var isRemoteAudioMuted: Bool public var localVideo: VideoSource? public var remoteVideo: VideoSource? public var isRemoteBatteryLow: Bool @@ -111,7 +68,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { shortName: String, avatarImage: UIImage?, audioOutput: AudioOutput, - isMicrophoneMuted: Bool, + isLocalAudioMuted: Bool, + isRemoteAudioMuted: Bool, localVideo: VideoSource?, remoteVideo: VideoSource?, isRemoteBatteryLow: Bool @@ -121,7 +79,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.shortName = shortName self.avatarImage = avatarImage self.audioOutput = audioOutput - self.isMicrophoneMuted = isMicrophoneMuted + self.isLocalAudioMuted = isLocalAudioMuted + self.isRemoteAudioMuted = isRemoteAudioMuted self.localVideo = localVideo self.remoteVideo = remoteVideo self.isRemoteBatteryLow = isRemoteBatteryLow @@ -143,7 +102,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView { if lhs.audioOutput != rhs.audioOutput { return false } - if lhs.isMicrophoneMuted != rhs.isMicrophoneMuted { + if lhs.isLocalAudioMuted != rhs.isLocalAudioMuted { + return false + } + if lhs.isRemoteAudioMuted != rhs.isRemoteAudioMuted { return false } if lhs.localVideo !== rhs.localVideo { @@ -162,12 +124,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView { private struct Params: Equatable { var size: CGSize var insets: UIEdgeInsets + var interfaceOrientation: UIInterfaceOrientation var screenCornerRadius: CGFloat var state: State - init(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State) { + init(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State) { self.size = size self.insets = insets + self.interfaceOrientation = interfaceOrientation self.screenCornerRadius = screenCornerRadius self.state = state } @@ -204,11 +168,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView { private var activeLocalVideoSource: VideoSource? private var waitingForFirstLocalVideoFrameDisposable: Disposable? + private var isUpdating: Bool = false + private var canAnimateAudioLevel: Bool = false private var displayEmojiTooltip: Bool = false private var isEmojiKeyExpanded: Bool = false private var areControlsHidden: Bool = false private var swapLocalAndRemoteVideo: Bool = false + private var isPictureInPictureActive: Bool = false private var processedInitialAudioLevelBump: Bool = false private var audioLevelBump: Float = 0.0 @@ -224,6 +191,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView { public var microhoneMuteAction: (() -> Void)? public var endCallAction: (() -> Void)? public var backAction: (() -> Void)? + public var closeAction: (() -> Void)? + public var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)? + + private let pipView: PrivateCallPictureInPictureView + private var pipContentSource: AnyObject? + private var pipVideoCallViewController: UIViewController? + private var pipController: AVPictureInPictureController? public override init(frame: CGRect) { self.overlayContentsView = UIView() @@ -249,6 +223,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.backButtonView = BackButtonView(text: "Back") + self.pipView = PrivateCallPictureInPictureView(frame: CGRect(origin: CGPoint(), size: CGSize())) + super.init(frame: frame) self.clipsToBounds = true @@ -264,10 +240,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.avatarTransformLayer.addSublayer(self.avatarLayer) self.layer.addSublayer(self.avatarTransformLayer) - /*let edgeTestLayer = EdgeTestLayer() - edgeTestLayer.frame = CGRect(origin: CGPoint(x: 20.0, y: 100.0), size: CGSize(width: 100.0, height: 100.0)) - self.layer.addSublayer(edgeTestLayer)*/ - self.addSubview(self.videoContainerBackgroundView) self.overlayContentsView.mask = self.maskContents @@ -310,6 +282,27 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } self.backAction?() } + + self.buttonGroupView.closePressed = { [weak self] in + guard let self else { + return + } + self.closeAction?() + } + + if #available(iOS 16.0, *) { + let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() + pipVideoCallViewController.view.addSubview(self.pipView) + self.pipView.frame = pipVideoCallViewController.view.bounds + self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.pipView.translatesAutoresizingMaskIntoConstraints = true + self.pipVideoCallViewController = pipVideoCallViewController + } + + if let blurFilter = makeBlurFilter() { + blurFilter.setValue(10.0 as NSNumber, forKey: "inputRadius") + self.overlayContentsView.layer.filters = [blurFilter] + } } public required init?(coder: NSCoder) { @@ -335,6 +328,39 @@ public final class PrivateCallScreen: OverlayMaskContainerView { return result } + public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.isPictureInPictureActive = true + if !self.isUpdating { + self.update(transition: .easeInOut(duration: 0.2)) + } + } + + public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.isPictureInPictureActive = false + if !self.isUpdating { + let wereControlsHidden = self.areControlsHidden + self.areControlsHidden = true + self.update(transition: .immediate) + + if !wereControlsHidden { + self.areControlsHidden = false + self.update(transition: .spring(duration: 0.4)) + } + } + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { + if self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil { + if let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture { + restoreUIForPictureInPicture(completionHandler) + } else { + completionHandler(false) + } + } else { + completionHandler(false) + } + } + public func addIncomingAudioLevel(value: Float) { if self.canAnimateAudioLevel { self.targetAudioLevel = value @@ -385,8 +411,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } - public func update(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State, transition: Transition) { - let params = Params(size: size, insets: insets, screenCornerRadius: screenCornerRadius, state: state) + public func beginPictureInPictureIfPossible() { + if let pipController = self.pipController, (self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil) { + pipController.startPictureInPicture() + } + } + + public func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State, transition: Transition) { + let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: screenCornerRadius, state: state) if self.params == params { return } @@ -487,6 +519,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } private func updateInternal(params: Params, transition: Transition) { + self.isUpdating = true + defer { + self.isUpdating = false + } + let genericAlphaTransition: Transition switch transition.animation { case .none: @@ -497,6 +534,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView { let backgroundFrame = CGRect(origin: CGPoint(), size: params.size) + let wideContentWidth: CGFloat + if params.size.width < 500.0 { + wideContentWidth = params.size.width - 44.0 * 2.0 + } else { + wideContentWidth = 400.0 + } + var activeVideoSources: [(VideoContainerView.Key, VideoSource)] = [] if self.swapLocalAndRemoteVideo { if let activeLocalVideoSource = self.activeLocalVideoSource { @@ -515,6 +559,41 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } let havePrimaryVideo = !activeVideoSources.isEmpty + if #available(iOS 16.0, *) { + if havePrimaryVideo, let pipVideoCallViewController = self.pipVideoCallViewController as? AVPictureInPictureVideoCallViewController { + if self.pipController == nil { + let pipContentSource = AVPictureInPictureController.ContentSource(activeVideoCallSourceView: self, contentViewController: pipVideoCallViewController) + let pipController = AVPictureInPictureController(contentSource: pipContentSource) + self.pipController = pipController + pipController.canStartPictureInPictureAutomaticallyFromInline = true + pipController.delegate = self + } + } else if let pipController = self.pipController { + self.pipController = nil + + if pipController.isPictureInPictureActive { + pipController.stopPictureInPicture() + } + + if self.isPictureInPictureActive { + self.isPictureInPictureActive = false + } + } + } + + self.pipView.isRenderingEnabled = self.isPictureInPictureActive + self.pipView.video = self.activeRemoteVideoSource ?? self.activeLocalVideoSource + if let pipVideoCallViewController = self.pipVideoCallViewController { + if let video = self.pipView.video, let currentOutput = video.currentOutput { + var rotatedResolution = currentOutput.resolution + let resolvedRotationAngle = currentOutput.rotationAngle + if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 { + rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) + } + pipVideoCallViewController.preferredContentSize = rotatedResolution + } + } + let currentAreControlsHidden = havePrimaryVideo && self.areControlsHidden let backgroundAspect: CGFloat = params.size.width / params.size.height @@ -554,7 +633,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } self.videoAction?() }), - ButtonGroupView.Button(content: .microphone(isMuted: params.state.isMicrophoneMuted), action: { [weak self] in + ButtonGroupView.Button(content: .microphone(isMuted: params.state.isLocalAudioMuted), action: { [weak self] in guard let self else { return } @@ -584,17 +663,24 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } var notices: [ButtonGroupView.Notice] = [] - if params.state.isMicrophoneMuted { + if params.state.isLocalAudioMuted { notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "Your microphone is turned off")) } + if params.state.isRemoteAudioMuted { + notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "\(params.state.shortName)'s microphone is turned off")) + } if params.state.remoteVideo != nil && params.state.localVideo == nil { - notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "Your camera is turned off")) + notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "Your camera is turned off")) } if params.state.isRemoteBatteryLow { - notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "\(params.state.shortName)'s battery is low")) + notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), text: "\(params.state.shortName)'s battery is low")) } - let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, controlsHidden: currentAreControlsHidden, buttons: buttons, notices: notices, transition: transition) + var displayClose = false + if case .terminated = params.state.lifecycleState { + displayClose = true + } + let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, buttons: buttons, notices: notices, transition: transition) var expandedEmojiKeyRect: CGRect? if self.isEmojiKeyExpanded { @@ -632,7 +718,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } - let emojiExpandedInfoSize = emojiExpandedInfoView.update(constrainedWidth: params.size.width - (params.insets.left + 16.0) * 2.0, transition: emojiExpandedInfoTransition) + let emojiExpandedInfoSize = emojiExpandedInfoView.update(width: wideContentWidth, transition: emojiExpandedInfoTransition) let emojiExpandedInfoFrame = CGRect(origin: CGPoint(x: floor((params.size.width - emojiExpandedInfoSize.width) * 0.5), y: params.insets.top + 73.0), size: emojiExpandedInfoSize) emojiExpandedInfoTransition.setPosition(view: emojiExpandedInfoView, position: CGPoint(x: emojiExpandedInfoFrame.minX + emojiExpandedInfoView.layer.anchorPoint.x * emojiExpandedInfoFrame.width, y: emojiExpandedInfoFrame.minY + emojiExpandedInfoView.layer.anchorPoint.y * emojiExpandedInfoFrame.height)) emojiExpandedInfoTransition.setBounds(view: emojiExpandedInfoView, bounds: CGRect(origin: CGPoint(), size: emojiExpandedInfoFrame.size)) @@ -855,7 +941,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { videoContainerView.blurredContainerLayer.position = self.avatarTransformLayer.position videoContainerView.blurredContainerLayer.bounds = self.avatarTransformLayer.bounds videoContainerView.blurredContainerLayer.opacity = 0.0 - videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate) + videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate) Transition.immediate.setScale(view: videoContainerView, scale: self.currentAvatarAudioScale) Transition.immediate.setScale(view: self.videoContainerBackgroundView, scale: self.currentAvatarAudioScale) } else { @@ -865,7 +951,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { videoContainerView.blurredContainerLayer.position = expandedVideoFrame.center videoContainerView.blurredContainerLayer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size) videoContainerView.blurredContainerLayer.opacity = 0.0 - videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate) + videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate) } } @@ -875,7 +961,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { videoContainerTransition.setPosition(layer: videoContainerView.blurredContainerLayer, position: expandedVideoFrame.center) videoContainerTransition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size)) videoContainerTransition.setScale(layer: videoContainerView.blurredContainerLayer, scale: 1.0) - videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition) + videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition) let alphaTransition: Transition switch transition.animation { @@ -897,8 +983,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } - alphaTransition.setAlpha(view: videoContainerView, alpha: 1.0) - alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0) + let videoAlpha: CGFloat = self.isPictureInPictureActive ? 0.0 : 1.0 + alphaTransition.setAlpha(view: videoContainerView, alpha: videoAlpha) + alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: videoAlpha) } var removedVideoContainerIndices: [Int] = [] @@ -910,7 +997,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { if self.videoContainerViews.count == 1 || (i == 0 && !havePrimaryVideo) { let alphaTransition: Transition = genericAlphaTransition - videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition) + videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition) transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center) transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0) @@ -949,7 +1036,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { }) alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0) - videoContainerView.update(size: params.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition) + videoContainerView.update(size: params.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition) } } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index f0c69d9a5e..a72251da55 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -71,6 +71,8 @@ private final class AnimatableProperty { let timeFromStart = timestamp - animation.startTimestamp var t = max(0.0, timeFromStart / duration) switch curve { + case .linear: + break case .easeInOut: t = listViewAnimationCurveEaseInOut(t) case .spring: diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 03b89e96e6..22d2cd0136 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -2455,7 +2455,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, - layoutMetrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), + layoutMetrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: DeviceMetrics.iPhone12, isVisible: true, isExpanded: false diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 1cd12e7d6a..2df3092fd4 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -70,7 +70,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { public var timerUpdated: ((NSNumber?) -> Void)? public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { - return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false) + return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) } public func caption() -> NSAttributedString { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift index a3a0913996..eca8b8c05e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift @@ -97,6 +97,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { let contextSourceNode: ContextReferenceContentNode private let textNode: ImmediateTextNode private let iconNode: ASImageNode + private let backIconLayer: SimpleShapeLayer private var animationNode: MoreIconNode? private let backgroundNode: NavigationBackgroundNode @@ -117,6 +118,15 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true + self.backIconLayer = SimpleShapeLayer() + self.backIconLayer.lineWidth = 3.0 + self.backIconLayer.lineCap = .round + self.backIconLayer.lineJoin = .round + self.backIconLayer.strokeColor = UIColor.white.cgColor + self.backIconLayer.fillColor = nil + self.backIconLayer.isHidden = true + self.backIconLayer.path = try? convertSvgPath("M10.5,2 L1.5,11 L10.5,20 ") + self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: true) super.init(pointerStyle: .insetRectangle(-8.0, 2.0)) @@ -128,6 +138,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { self.contextSourceNode.addSubnode(self.backgroundNode) self.contextSourceNode.addSubnode(self.textNode) self.contextSourceNode.addSubnode(self.iconNode) + self.contextSourceNode.layer.addSublayer(self.backIconLayer) self.addSubnode(self.containerNode) @@ -146,13 +157,43 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { self.action?(self.contextSourceNode, nil) } - func updateContentsColor(backgroundColor: UIColor, contentsColor: UIColor, transition: ContainedViewLayoutTransition) { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var boundingRect = self.bounds + if self.textNode.alpha != 0.0 { + boundingRect = boundingRect.union(self.textNode.frame) + } + boundingRect = boundingRect.insetBy(dx: -8.0, dy: -4.0) + if boundingRect.contains(point) { + return super.hitTest(self.bounds.center, with: event) + } else { + return nil + } + } + + func updateContentsColor(backgroundColor: UIColor, contentsColor: UIColor, canBeExpanded: Bool, transition: ContainedViewLayoutTransition) { self.contentsColor = contentsColor self.backgroundNode.updateColor(color: backgroundColor, transition: transition) transition.updateTintColor(layer: self.textNode.layer, color: self.contentsColor) transition.updateTintColor(layer: self.iconNode.layer, color: self.contentsColor) + transition.updateStrokeColor(layer: self.backIconLayer, strokeColor: self.contentsColor) + + switch self.key { + case .back: + transition.updateAlpha(layer: self.textNode.layer, alpha: canBeExpanded ? 1.0 : 0.0) + transition.updateTransformScale(node: self.textNode, scale: canBeExpanded ? 1.0 : 0.001) + + var iconTransform = CATransform3DIdentity + iconTransform = CATransform3DScale(iconTransform, canBeExpanded ? 1.0 : 0.8, canBeExpanded ? 1.0 : 0.8, 1.0) + iconTransform = CATransform3DTranslate(iconTransform, canBeExpanded ? -7.0 : 0.0, 0.0, 0.0) + transition.updateTransform(node: self.iconNode, transform: CATransform3DGetAffineTransform(iconTransform)) + + transition.updateTransform(layer: self.backIconLayer, transform: CATransform3DGetAffineTransform(iconTransform)) + transition.updateLineWidth(layer: self.backIconLayer, lineWidth: canBeExpanded ? 3.0 : 2.075) + default: + break + } if let animationNode = self.animationNode { transition.updateTintColor(layer: animationNode.imageNode.layer, color: self.contentsColor) @@ -184,9 +225,9 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { var animationState: MoreIconNodeState = .more switch key { case .back: - text = "" + text = presentationData.strings.Common_Back accessibilityText = presentationData.strings.Common_Back - icon = NavigationBar.thinBackArrowImage + icon = NavigationBar.backArrowImage(color: .white) case .edit: text = presentationData.strings.Common_Edit accessibilityText = text @@ -270,11 +311,19 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { } let inset: CGFloat = 0.0 + var textInset: CGFloat = 0.0 + switch key { + case .back: + textInset += 11.0 + default: + break + } let resultSize: CGSize - let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize) - self.textNode.frame = textFrame + let textFrame = CGRect(origin: CGPoint(x: inset + textInset, y: floor((height - textSize.height) / 2.0)), size: textSize) + self.textNode.position = textFrame.center + self.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size) if let animationNode = self.animationNode { let animationSize = CGSize(width: 30.0, height: 30.0) @@ -286,7 +335,20 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) resultSize = size } else if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size).offsetBy(dx: iconOffset.x, dy: iconOffset.y) + let iconFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size).offsetBy(dx: iconOffset.x, dy: iconOffset.y) + self.iconNode.position = iconFrame.center + self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + if case .back = key { + self.backIconLayer.position = iconFrame.center + self.backIconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconNode.isHidden = true + self.backIconLayer.isHidden = false + } else { + self.iconNode.isHidden = false + self.backIconLayer.isHidden = true + } let size = CGSize(width: image.size.width + inset * 2.0, height: height) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift index c12dd1e360..18b3dd3f19 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift @@ -36,18 +36,21 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { private var backgroundContentColor: UIColor = .clear private var contentsColor: UIColor = .white + private var canBeExpanded: Bool = false var performAction: ((PeerInfoHeaderNavigationButtonKey, ContextReferenceContentNode?, ContextGesture?) -> Void)? - func updateContentsColor(backgroundContentColor: UIColor, contentsColor: UIColor, transition: ContainedViewLayoutTransition) { + func updateContentsColor(backgroundContentColor: UIColor, contentsColor: UIColor, canBeExpanded: Bool, transition: ContainedViewLayoutTransition) { self.backgroundContentColor = backgroundContentColor self.contentsColor = contentsColor for (_, button) in self.leftButtonNodes { - button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: transition) + button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: canBeExpanded, transition: transition) + transition.updateSublayerTransformOffset(layer: button.layer, offset: CGPoint(x: canBeExpanded ? -8.0 : 0.0, y: 0.0)) } for (_, button) in self.rightButtonNodes { - button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: transition) + button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: canBeExpanded, transition: transition) + transition.updateSublayerTransformOffset(layer: button.layer, offset: CGPoint(x: canBeExpanded ? 8.0 : 0.0, y: 0.0)) } } @@ -106,7 +109,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { buttonNode.frame = buttonFrame buttonNode.alpha = 0.0 transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: .immediate) + buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: self.canBeExpanded, transition: .immediate) } else { transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) @@ -202,7 +205,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { } let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) if wasAdded { - buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: .immediate) + buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: self.canBeExpanded, transition: .immediate) if key == .moreToSearch { buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index c6bb1f3716..18af6ed860 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -553,6 +553,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let navigationContentsAccentColor: UIColor let navigationContentsPrimaryColor: UIColor let navigationContentsSecondaryColor: UIColor + let navigationContentsCanBeExpanded: Bool let contentButtonBackgroundColor: UIColor let contentButtonForegroundColor: UIColor @@ -640,6 +641,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { navigationContentsAccentColor = collapsedHeaderNavigationContentsAccentColor navigationContentsPrimaryColor = collapsedHeaderNavigationContentsPrimaryColor navigationContentsSecondaryColor = collapsedHeaderNavigationContentsSecondaryColor + navigationContentsCanBeExpanded = true + contentButtonBackgroundColor = collapsedHeaderContentButtonBackgroundColor contentButtonForegroundColor = collapsedHeaderContentButtonForegroundColor @@ -651,6 +654,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { contentButtonBackgroundColor = expandedAvatarContentButtonBackgroundColor contentButtonForegroundColor = expandedAvatarContentButtonForegroundColor + navigationContentsCanBeExpanded = false + headerButtonBackgroundColor = expandedAvatarHeaderButtonBackgroundColor } else { let effectiveTransitionFraction: CGFloat = innerBackgroundTransitionFraction < 0.5 ? 0.0 : 1.0 @@ -659,6 +664,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { navigationContentsPrimaryColor = regularNavigationContentsPrimaryColor.mixedWith(collapsedHeaderNavigationContentsPrimaryColor, alpha: effectiveTransitionFraction) navigationContentsSecondaryColor = regularNavigationContentsSecondaryColor.mixedWith(collapsedHeaderNavigationContentsSecondaryColor, alpha: effectiveTransitionFraction) + if peer?.profileColor != nil { + navigationContentsCanBeExpanded = effectiveTransitionFraction == 1.0 + } else { + navigationContentsCanBeExpanded = true + } + contentButtonBackgroundColor = regularContentButtonBackgroundColor//.mixedWith(collapsedHeaderContentButtonBackgroundColor, alpha: effectiveTransitionFraction) contentButtonForegroundColor = regularContentButtonForegroundColor//.mixedWith(collapsedHeaderContentButtonForegroundColor, alpha: effectiveTransitionFraction) @@ -775,7 +786,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.titleExpandedCredibilityIconSize = expandedIconSize } - self.navigationButtonContainer.updateContentsColor(backgroundContentColor: headerButtonBackgroundColor, contentsColor: navigationContentsAccentColor, transition: navigationTransition) + self.navigationButtonContainer.updateContentsColor(backgroundContentColor: headerButtonBackgroundColor, contentsColor: navigationContentsAccentColor, canBeExpanded: navigationContentsCanBeExpanded, transition: navigationTransition) self.titleNode.updateTintColor(color: navigationContentsPrimaryColor, transition: navigationTransition) self.subtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index 2964864ea0..c82be4bbb9 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -844,7 +844,7 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD return } - if let (updatedUrlPreviewState, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil), let updatedUrlPreviewState, let detectedUrl = updatedUrlPreviewState.detectedUrls.first { + if let (updatedUrlPreviewState, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil, forPeerId: selfController.chatLocation.peerId), let updatedUrlPreviewState, let detectedUrl = updatedUrlPreviewState.detectedUrls.first { if let webpage = webpageCache[detectedUrl] { progress?.set(.single(false)) diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index 9f6beaff47..2b7777d409 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -220,7 +220,7 @@ func updateChatPresentationInterfaceStateImpl( } } - if let (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0) { + if let (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) { selfController.urlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)? @@ -301,7 +301,7 @@ func updateChatPresentationInterfaceStateImpl( let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext let editingUrlPreviewText: NSAttributedString? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText - if let (updatedEditingUrlPreviewState, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: selfController.context, currentQuery: selfController.editingUrlPreviewQueryState?.0) { + if let (updatedEditingUrlPreviewState, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: selfController.context, currentQuery: selfController.editingUrlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) { selfController.editingUrlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)? diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 29b95179ec..ead219146a 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -510,7 +510,7 @@ struct UrlPreviewState { var detectedUrls: [String] } -func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: UrlPreviewState?) -> (UrlPreviewState?, Signal<(TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?, NoError>)? { +func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: UrlPreviewState?, forPeerId: PeerId?) -> (UrlPreviewState?, Signal<(TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?, NoError>)? { guard let _ = inputText else { if currentQuery != nil { return (nil, .single({ _ in return nil })) @@ -522,7 +522,7 @@ func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: Acco let detectedUrls = detectUrls(inputText) if detectedUrls != (currentQuery?.detectedUrls ?? []) { if !detectedUrls.isEmpty { - return (UrlPreviewState(detectedUrls: detectedUrls), webpagePreview(account: context.account, urls: detectedUrls) + return (UrlPreviewState(detectedUrls: detectedUrls), webpagePreview(account: context.account, urls: detectedUrls, forPeerId: forPeerId) |> mapToSignal { result -> Signal<(TelegramMediaWebpage, String)?, NoError> in guard case let .result(webpageResult) = result else { return .complete() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 7e6749f8e7..a5b1e63fc5 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -132,8 +132,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { private var groupCallDisposable: Disposable? private var callController: CallController? + private var call: PresentationCall? public let hasOngoingCall = ValuePromise(false) private let callState = Promise(nil) + private var awaitingCallConnectionDisposable: Disposable? private var groupCallController: VoiceChatController? public var currentGroupCallController: ViewController? { @@ -741,26 +743,49 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callDisposable = (callManager.currentCallSignal |> deliverOnMainQueue).start(next: { [weak self] call in - if let strongSelf = self { - if call !== strongSelf.callController?.call { - strongSelf.callController?.dismiss() - strongSelf.callController = nil - strongSelf.hasOngoingCall.set(false) + guard let self else { + return + } + + if call !== self.call { + self.call = call + + self.callController?.dismiss() + self.callController = nil + self.hasOngoingCall.set(false) + + if let call { + self.callState.set(call.state + |> map(Optional.init)) + self.hasOngoingCall.set(true) + setNotificationCall(call) - if let call = call { - mainWindow.hostView.containerView.endEditing(true) - let callController = CallController(sharedContext: strongSelf, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) - strongSelf.callController = callController - strongSelf.mainWindow?.present(callController, on: .calls) - strongSelf.callState.set(call.state - |> map(Optional.init)) - strongSelf.hasOngoingCall.set(true) - setNotificationCall(call) - } else { - strongSelf.callState.set(.single(nil)) - strongSelf.hasOngoingCall.set(false) - setNotificationCall(nil) + if !call.isOutgoing && call.isIntegratedWithCallKit { + self.awaitingCallConnectionDisposable = (call.state + |> filter { state in + switch state.state { + case .ringing: + return false + default: + return true + } + } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + self.presentControllerWithCurrentCall() + }) + } else{ + self.presentControllerWithCurrentCall() } + } else { + self.callState.set(.single(nil)) + self.hasOngoingCall.set(false) + self.awaitingCallConnectionDisposable?.dispose() + self.awaitingCallConnectionDisposable = nil + setNotificationCall(nil) } } }) @@ -951,6 +976,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callDisposable?.dispose() self.groupCallDisposable?.dispose() self.callStateDisposable?.dispose() + self.awaitingCallConnectionDisposable?.dispose() } private var didPerformAccountSettingsImport = false @@ -1010,6 +1036,37 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } + private func presentControllerWithCurrentCall() { + guard let call = self.call else { + return + } + + if let currentCallController = self.callController { + if currentCallController.call === call { + self.navigateToCurrentCall() + return + } else { + self.callController = nil + currentCallController.dismiss() + } + } + + self.mainWindow?.hostView.containerView.endEditing(true) + let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) + self.callController = callController + callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in + guard let self, let callController else { + completion(false) + return + } + if callController.window == nil { + self.mainWindow?.present(callController, on: .calls) + } + completion(true) + } + self.mainWindow?.present(callController, on: .calls) + } + public func updateNotificationTokensRegistration() { let sandbox: Bool #if DEBUG diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index fa2e53f5da..6b73742cdc 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit fa2e53f5da9b9653ab47169a922fb6c82847134a +Subproject commit 6b73742cdc140c46a1ab1b8e3390354a9738e429 diff --git a/submodules/Utils/LokiRng/Package.swift b/submodules/Utils/LokiRng/Package.swift index f2c2bcf28a..db37e9f384 100644 --- a/submodules/Utils/LokiRng/Package.swift +++ b/submodules/Utils/LokiRng/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "LokiRng", + platforms: [.macOS(.v10_13)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( diff --git a/submodules/Utils/ShelfPack/Package.swift b/submodules/Utils/ShelfPack/Package.swift index 871e14c64b..9bd42def11 100644 --- a/submodules/Utils/ShelfPack/Package.swift +++ b/submodules/Utils/ShelfPack/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "ShelfPack", + platforms: [.macOS(.v10_13)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -23,5 +24,6 @@ let package = Package( dependencies: [], path: ".", publicHeadersPath: "PublicHeaders"), - ] + ], + cxxLanguageStandard: .cxx20 )