mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
1f1c8758f1
@ -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",
|
||||
]
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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?) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -925,3 +925,68 @@ public func drawSvgPath(_ context: CGContext, path: StaticString, strokeOnMove:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func convertSvgPath(_ path: StaticString) throws -> CGPath {
|
||||
var index: UnsafePointer<UInt8> = 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
|
||||
}
|
||||
|
@ -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<WindowUserInterfaceStyle>(ignoreRepeated: true)
|
||||
var systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError> {
|
||||
@ -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
|
||||
|
@ -156,6 +156,7 @@ public final class WindowHostView {
|
||||
public let eventView: UIView
|
||||
public let isRotating: () -> Bool
|
||||
public let systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError>
|
||||
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<WindowUserInterfaceStyle, NoError>, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) {
|
||||
init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError>, 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
|
||||
|
@ -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,
|
||||
|
@ -2,6 +2,7 @@
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
import TGUIKit
|
||||
#endif
|
||||
import CoreMedia
|
||||
import Accelerate
|
||||
|
@ -3,6 +3,7 @@ import Foundation
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
import TGUIKit
|
||||
#endif
|
||||
import CoreMedia
|
||||
import SwiftSignalKit
|
||||
|
@ -3,6 +3,7 @@ import Foundation
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
import TGUIKit
|
||||
#endif
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
|
@ -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/"),
|
||||
]
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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<PathMonitor>(queue: queue, generate: {
|
||||
@ -149,7 +149,7 @@ public enum Reachability {
|
||||
}
|
||||
|
||||
public static var networkType: Signal<NetworkType, NoError> {
|
||||
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()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<WebpagePreviewResult, NoError> {
|
||||
return webpagePreviewWithProgress(account: account, urls: urls)
|
||||
|
||||
public func webpagePreview(account: Account, urls: [String], webpageId: MediaId? = nil, forPeerId: PeerId? = nil) -> Signal<WebpagePreviewResult, NoError> {
|
||||
return webpagePreviewWithProgress(account: account, urls: urls, webpageId: webpageId, forPeerId: forPeerId)
|
||||
|> mapToSignal { next -> Signal<WebpagePreviewResult, NoError> 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<WebpagePreviewWithProgressResult, NoError> {
|
||||
public func webpagePreviewWithProgress(account: Account, urls: [String], webpageId: MediaId? = nil, forPeerId: PeerId? = nil) -> Signal<WebpagePreviewWithProgressResult, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<WebpagePreviewWithProgressResult, NoError> 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<NetworkRequestResult<Api.MessageMedia>, NoError> in
|
||||
return .single(.result(.messageMediaEmpty))
|
||||
|
@ -67,6 +67,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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<half, access::read> inTextureY [[ texture(0) ]],
|
||||
texture2d<half, access::read> inTextureUV [[ texture(1) ]],
|
||||
texture2d<half, access::write> outTexture [[ texture(2) ]],
|
||||
@ -249,6 +249,22 @@ kernel void videoYUVToRGBA(
|
||||
outTexture.write(color, threadPosition);
|
||||
}
|
||||
|
||||
kernel void videoTriPlanarToRGBA(
|
||||
texture2d<half, access::read> inTextureY [[ texture(0) ]],
|
||||
texture2d<half, access::read> inTextureU [[ texture(1) ]],
|
||||
texture2d<half, access::read> inTextureV [[ texture(2) ]],
|
||||
texture2d<half, access::write> 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) ]],
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<Float> = SIMD4<Float>(1.0, 1.0, 1.0, 0.2)
|
||||
encoder.setFragmentBytes(&brightness, length: 4, index: 0)
|
||||
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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>(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<Float>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,8 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
let timeFromStart = timestamp - animation.startTimestamp
|
||||
var t = max(0.0, timeFromStart / duration)
|
||||
switch curve {
|
||||
case .linear:
|
||||
break
|
||||
case .easeInOut:
|
||||
t = listViewAnimationCurveEaseInOut(t)
|
||||
case .spring:
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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)?)?
|
||||
|
@ -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()
|
||||
|
@ -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<Bool>(false)
|
||||
private let callState = Promise<PresentationCallState?>(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
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit fa2e53f5da9b9653ab47169a922fb6c82847134a
|
||||
Subproject commit 6b73742cdc140c46a1ab1b8e3390354a9738e429
|
@ -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(
|
||||
|
@ -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
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user