Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-12-11 14:28:05 +04:00
commit 1f1c8758f1
53 changed files with 1694 additions and 272 deletions

View File

@ -443,6 +443,7 @@ official_apple_pay_merchants = [
"merchant.org.telegram.billinenet.test", "merchant.org.telegram.billinenet.test",
"merchant.org.telegram.billinenet.prod", "merchant.org.telegram.billinenet.prod",
"merchant.org.telegram.portmone.test", "merchant.org.telegram.portmone.test",
"merchant.org.telegram.portmone.prod",
"merchant.org.telegram.ecommpay.test", "merchant.org.telegram.ecommpay.test",
] ]

View File

@ -29,7 +29,8 @@ public final class ViewController: UIViewController {
shortName: "Emma", shortName: "Emma",
avatarImage: UIImage(named: "test"), avatarImage: UIImage(named: "test"),
audioOutput: .internalSpeaker, audioOutput: .internalSpeaker,
isMicrophoneMuted: false, isLocalAudioMuted: false,
isRemoteAudioMuted: false,
localVideo: nil, localVideo: nil,
remoteVideo: nil, remoteVideo: nil,
isRemoteBatteryLow: false isRemoteBatteryLow: false
@ -142,30 +143,39 @@ public final class ViewController: UIViewController {
self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0)) self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0))
self.callState.remoteVideo = nil self.callState.remoteVideo = nil
self.callState.localVideo = nil self.callState.localVideo = nil
self.callState.isLocalAudioMuted = false
self.callState.isRemoteBatteryLow = false
self.update(transition: .spring(duration: 0.4)) self.update(transition: .spring(duration: 0.4))
} }
callScreenView.backAction = { [weak self] in callScreenView.backAction = { [weak self] in
guard let self else { guard let self else {
return return
} }
self.callState.isMicrophoneMuted = !self.callState.isMicrophoneMuted //self.callState.isLocalAudioMuted = !self.callState.isLocalAudioMuted
self.update(transition: .spring(duration: 0.4)) //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) { private func update(transition: Transition) {
if let (size, insets) = self.currentLayout { 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 { guard let callScreenView = self.callScreenView else {
return return
} }
transition.setFrame(view: callScreenView, frame: CGRect(origin: CGPoint(), size: size)) 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() { override public func viewWillLayoutSubviews() {
@ -182,7 +192,7 @@ public final class ViewController: UIViewController {
if let currentLayout = self.currentLayout, currentLayout == (size, insets) { if let currentLayout = self.currentLayout, currentLayout == (size, insets) {
} else { } else {
self.currentLayout = (size, insets) self.currentLayout = (size, insets)
self.update(size: size, insets: insets, transition: transition) self.update(size: size, insets: insets, interfaceOrientation: self.interfaceOrientation, transition: transition)
} }
} }

View File

@ -507,7 +507,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
guard let presentationInterfaceState = self.presentationInterfaceState else { guard let presentationInterfaceState = self.presentationInterfaceState else {
return 0.0 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?) { public func setCaption(_ caption: NSAttributedString?) {

View File

@ -49,6 +49,8 @@ private extension Transition.Animation.Curve {
switch self { switch self {
case .easeInOut: case .easeInOut:
return CAMediaTimingFunction(name: .easeInEaseOut) return CAMediaTimingFunction(name: .easeInEaseOut)
case .linear:
return CAMediaTimingFunction(name: .linear)
case let .custom(a, b, c, d): case let .custom(a, b, c, d):
return CAMediaTimingFunction(controlPoints: a, b, c, d) return CAMediaTimingFunction(controlPoints: a, b, c, d)
case .spring: case .spring:
@ -72,6 +74,7 @@ public struct Transition {
public enum Curve { public enum Curve {
case easeInOut case easeInOut
case spring case spring
case linear
case custom(Float, Float, Float, Float) case custom(Float, Float, Float, Float)
public func solve(at offset: CGFloat) -> CGFloat { public func solve(at offset: CGFloat) -> CGFloat {
@ -80,6 +83,8 @@ public struct Transition {
return listViewAnimationCurveEaseInOut(offset) return listViewAnimationCurveEaseInOut(offset)
case .spring: case .spring:
return listViewAnimationCurveSystem(offset) return listViewAnimationCurveSystem(offset)
case .linear:
return offset
case let .custom(c1x, c1y, c2x, c2y): case let .custom(c1x, c1y, c2x, c2y):
return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset) return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset)
} }

View File

@ -7,7 +7,7 @@ public extension Transition.Animation.Curve {
init(_ curve: ContainedViewLayoutTransitionCurve) { init(_ curve: ContainedViewLayoutTransitionCurve) {
switch curve { switch curve {
case .linear: case .linear:
self = .easeInOut self = .linear
case .easeInOut: case .easeInOut:
self = .easeInOut self = .easeInOut
case let .custom(a, b, c, d): case let .custom(a, b, c, d):
@ -21,6 +21,8 @@ public extension Transition.Animation.Curve {
var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve { var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve {
switch self { switch self {
case .linear:
return .linear
case .easeInOut: case .easeInOut:
return .easeInOut return .easeInOut
case .spring: case .spring:

View File

@ -1776,7 +1776,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega
} }
contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight)) 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 contentUnscaledSize = preferredSize
} }
} else { } else {
@ -1786,7 +1786,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega
let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset 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)) 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 contentUnscaledSize = preferredSize
} }
} }

View File

@ -183,7 +183,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.controller.containerLayoutUpdated( self.controller.containerLayoutUpdated(
ContainerViewLayout( ContainerViewLayout(
size: size, size: size,
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil),
deviceMetrics: parentLayout.deviceMetrics, deviceMetrics: parentLayout.deviceMetrics,
intrinsicInsets: UIEdgeInsets(), intrinsicInsets: UIEdgeInsets(),
safeInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(),
@ -765,7 +765,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
defaultContentSize.height = min(defaultContentSize.height, 460.0) defaultContentSize.height = min(defaultContentSize.height, 460.0)
let contentSize: CGSize 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 contentSize = preferredSize
} else if let storedContentHeight = contentNode.storedContentHeight { } else if let storedContentHeight = contentNode.storedContentHeight {
contentSize = CGSize(width: defaultContentSize.width, height: storedContentHeight) contentSize = CGSize(width: defaultContentSize.width, height: storedContentHeight)

View File

@ -69,13 +69,15 @@ public func childWindowHostView(parent: UIView) -> WindowHostView {
let hostView = WindowHostView(containerView: view, eventView: view, isRotating: { let hostView = WindowHostView(containerView: view, eventView: view, isRotating: {
return false return false
}, systemUserInterfaceStyle: .single(.light), updateSupportedInterfaceOrientations: { orientations in }, systemUserInterfaceStyle: .single(.light), currentInterfaceOrientation: {
return .portrait
}, updateSupportedInterfaceOrientations: { orientations in
}, updateDeferScreenEdgeGestures: { edges in }, updateDeferScreenEdgeGestures: { edges in
}, updatePrefersOnScreenNavigationHidden: { value in }, updatePrefersOnScreenNavigationHidden: { value in
}) })
view.updateSize = { [weak hostView] size in view.updateSize = { [weak hostView] size in
hostView?.updateSize?(size, 0.0) hostView?.updateSize?(size, 0.0, .portrait)
} }
view.layoutSubviewsEvent = { [weak hostView] in view.layoutSubviewsEvent = { [weak hostView] in

View File

@ -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) { func attachAnimation(view: UIView, id: String, completion: @escaping (Bool) -> Void) {
switch self { switch self {
case .immediate: case .immediate:

View File

@ -23,15 +23,18 @@ public enum ContainerViewLayoutSizeClass {
public struct LayoutMetrics: Equatable { public struct LayoutMetrics: Equatable {
public let widthClass: ContainerViewLayoutSizeClass public let widthClass: ContainerViewLayoutSizeClass
public let heightClass: 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.widthClass = widthClass
self.heightClass = heightClass self.heightClass = heightClass
self.orientation = orientation
} }
public init() { public init() {
self.widthClass = .compact self.widthClass = .compact
self.heightClass = .compact self.heightClass = .compact
self.orientation = nil
} }
} }

View File

@ -22,7 +22,7 @@ public final class ContextContentContainerNode: ASDisplayNode {
transition.updateBounds(node: controller, bounds: CGRect(origin: CGPoint(), size: size)) transition.updateBounds(node: controller, bounds: CGRect(origin: CGPoint(), size: size))
transition.updateTransformScale(node: controller, scale: scaledSize.width / size.width) transition.updateTransformScale(node: controller, scale: scaledSize.width / size.width)
controller.updateLayout(size: size, transition: transition) 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)
} }
} }
} }

View File

@ -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
}

View File

@ -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 { public enum WindowUserInterfaceStyle {
case light case light
case dark case dark
@ -74,7 +92,7 @@ private final class WindowRootViewController: UIViewController, UIViewController
private var registeredForPreviewing = false private var registeredForPreviewing = false
var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)? 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) private var _systemUserInterfaceStyle = ValuePromise<WindowUserInterfaceStyle>(ignoreRepeated: true)
var systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError> { var systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError> {
@ -182,8 +200,10 @@ private final class WindowRootViewController: UIViewController, UIViewController
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator) super.viewWillTransition(to: size, with: coordinator)
let orientation = getCurrentViewInterfaceOrientation(view: self.view)
UIView.performWithoutAnimation { 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.view.frame = CGRect(origin: CGPoint(), size: window.bounds.size)
rootViewController.viewDidAppear(false) rootViewController.viewDidAppear(false)
let hostView = WindowHostView(containerView: rootViewController.view, eventView: window, isRotating: { let hostView = WindowHostView(
containerView: rootViewController.view,
eventView: window,
isRotating: {
return window.isRotating() return window.isRotating()
}, systemUserInterfaceStyle: rootViewController.systemUserInterfaceStyle, updateSupportedInterfaceOrientations: { orientations in },
systemUserInterfaceStyle: rootViewController.systemUserInterfaceStyle,
currentInterfaceOrientation: {
return getCurrentViewInterfaceOrientation(view: window)
},
updateSupportedInterfaceOrientations: { orientations in
rootViewController.orientations = orientations rootViewController.orientations = orientations
}, updateDeferScreenEdgeGestures: { edges in },
updateDeferScreenEdgeGestures: { edges in
rootViewController.gestureEdges = edges rootViewController.gestureEdges = edges
}, updatePrefersOnScreenNavigationHidden: { value in },
updatePrefersOnScreenNavigationHidden: { value in
rootViewController.prefersOnScreenNavigationHidden = value rootViewController.prefersOnScreenNavigationHidden = value
}) }
)
rootViewController.transitionToSize = { [weak hostView] size, duration in rootViewController.transitionToSize = { [weak hostView] size, duration, orientation in
hostView?.updateSize?(size, duration) hostView?.updateSize?(size, duration, orientation)
} }
window.updateSize = { _ in window.updateSize = { _ in

View File

@ -156,6 +156,7 @@ public final class WindowHostView {
public let eventView: UIView public let eventView: UIView
public let isRotating: () -> Bool public let isRotating: () -> Bool
public let systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError> public let systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError>
public let currentInterfaceOrientation: () -> UIInterfaceOrientation
let updateSupportedInterfaceOrientations: (UIInterfaceOrientationMask) -> Void let updateSupportedInterfaceOrientations: (UIInterfaceOrientationMask) -> Void
let updateDeferScreenEdgeGestures: (UIRectEdge) -> Void let updateDeferScreenEdgeGestures: (UIRectEdge) -> Void
@ -166,7 +167,7 @@ public final class WindowHostView {
var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)? var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)?
var presentNative: ((UIViewController) -> Void)? var presentNative: ((UIViewController) -> Void)?
var nativeController: (() -> UIViewController?)? var nativeController: (() -> UIViewController?)?
var updateSize: ((CGSize, Double) -> Void)? var updateSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)?
var layoutSubviews: (() -> Void)? var layoutSubviews: (() -> Void)?
var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)? var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)?
var isUpdatingOrientationLayout = false var isUpdatingOrientationLayout = false
@ -178,11 +179,12 @@ public final class WindowHostView {
var forEachController: (((ContainableController) -> Void) -> Void)? var forEachController: (((ContainableController) -> Void) -> Void)?
var getAccessibilityElements: (() -> [Any]?)? 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.containerView = containerView
self.eventView = eventView self.eventView = eventView
self.isRotating = isRotating self.isRotating = isRotating
self.systemUserInterfaceStyle = systemUserInterfaceStyle self.systemUserInterfaceStyle = systemUserInterfaceStyle
self.currentInterfaceOrientation = currentInterfaceOrientation
self.updateSupportedInterfaceOrientations = updateSupportedInterfaceOrientations self.updateSupportedInterfaceOrientations = updateSupportedInterfaceOrientations
self.updateDeferScreenEdgeGestures = updateDeferScreenEdgeGestures self.updateDeferScreenEdgeGestures = updateDeferScreenEdgeGestures
self.updatePrefersOnScreenNavigationHidden = updatePrefersOnScreenNavigationHidden 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 { if size.width > 690.0 && size.height > 650.0 {
return LayoutMetrics(widthClass: .regular, heightClass: .regular) return LayoutMetrics(widthClass: .regular, heightClass: .regular, orientation: orientation)
} else { } else {
return LayoutMetrics(widthClass: .compact, heightClass: .compact) return LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: orientation)
} }
} }
@ -354,7 +356,9 @@ public class Window1 {
let safeInsets = self.deviceMetrics.safeInsets(inLandscape: isLandscape) let safeInsets = self.deviceMetrics.safeInsets(inLandscape: isLandscape)
let onScreenNavigationHeight = self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight) 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.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate)
self.presentationContext = PresentationContext() self.presentationContext = PresentationContext()
self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost, parentView: self.hostView.containerView) self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost, parentView: self.hostView.containerView)
@ -406,8 +410,8 @@ public class Window1 {
self?.presentNative(controller) self?.presentNative(controller)
} }
self.hostView.updateSize = { [weak self] size, duration in self.hostView.updateSize = { [weak self] size, duration, orientation in
self?.updateSize(size, duration: duration) self?.updateSize(size, duration: duration, orientation: orientation)
} }
self.hostView.layoutSubviews = { [weak self] in self.hostView.layoutSubviews = { [weak self] in
@ -807,14 +811,14 @@ public class Window1 {
return self.viewController?.view.hitTest(point, with: event) 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 let transition: ContainedViewLayoutTransition
if !duration.isZero { if !duration.isZero {
transition = .animated(duration: duration, curve: .easeInOut) transition = .animated(duration: duration, curve: .easeInOut)
} else { } else {
transition = .immediate 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 { if let statusBarHost = self.statusBarHost, !statusBarHost.isApplicationInForeground {
self.layoutSubviews(force: true) self.layoutSubviews(force: true)
} }
@ -1117,7 +1121,7 @@ public class Window1 {
} }
let previousInputOffset = inputHeightOffsetForLayout(self.windowLayout) 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 childLayout = containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics)
let childLayoutUpdated = self.updatedContainerLayout != childLayout let childLayoutUpdated = self.updatedContainerLayout != childLayout

View File

@ -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) { public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) {
let layout = ContainerViewLayout( let layout = ContainerViewLayout(
size: size, 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), deviceMetrics: DeviceMetrics(screenSize: size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight, onScreenNavigationHeight: nil),
intrinsicInsets: intrinsicInsets, intrinsicInsets: intrinsicInsets,
safeInsets: safeInsets, safeInsets: safeInsets,

View File

@ -2,6 +2,7 @@
import UIKit import UIKit
#else #else
import AppKit import AppKit
import TGUIKit
#endif #endif
import CoreMedia import CoreMedia
import Accelerate import Accelerate

View File

@ -3,6 +3,7 @@ import Foundation
import UIKit import UIKit
#else #else
import AppKit import AppKit
import TGUIKit
#endif #endif
import CoreMedia import CoreMedia
import SwiftSignalKit import SwiftSignalKit

View File

@ -3,6 +3,7 @@ import Foundation
import UIKit import UIKit
#else #else
import AppKit import AppKit
import TGUIKit
#endif #endif
import SwiftSignalKit import SwiftSignalKit
import Postbox import Postbox

View File

@ -5,6 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "MetalEngine", name: "MetalEngine",
platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(
@ -13,6 +14,7 @@ let package = Package(
], ],
dependencies: [ dependencies: [
.package(name: "ShelfPack", path: "../Utils/ShelfPack"), .package(name: "ShelfPack", path: "../Utils/ShelfPack"),
.package(name: "TGUIKit", path: "../../../../packages/TGUIKit"),
// Dependencies declare other packages that this package depends on. // Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"), // .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. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "MetalEngine", name: "MetalEngine",
dependencies: [.product(name: "ShelfPack", package: "ShelfPack", condition: nil)], dependencies: [.product(name: "ShelfPack", package: "ShelfPack", condition: nil),
path: "Sources/MetalEngine"), .product(name: "TGUIKit", package: "TGUIKit", condition: nil)],
path: "Sources/"),
] ]
) )

View File

@ -4,8 +4,12 @@ import Metal
#if os(iOS) #if os(iOS)
import Display import Display
import UIKit import UIKit
#else
import AppKit
import TGUIKit
#endif #endif
import IOSurface import IOSurface
import ShelfPack import ShelfPack
@ -662,13 +666,6 @@ public final class MetalEngine {
fileprivate var computeStates: [ObjectIdentifier: ComputeState] = [:] fileprivate var computeStates: [ObjectIdentifier: ComputeState] = [:]
init?(device: MTLDevice) { 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 self.device = device
@ -677,15 +674,42 @@ public final class MetalEngine {
} }
self.commandQueue = commandQueue self.commandQueue = commandQueue
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { let library: MTLLibrary?
return nil
}
self.library = library
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 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 return nil
} }

View File

@ -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 final class PathMonitor {
private let queue: Queue private let queue: Queue
private let monitor: NWPathMonitor 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 { private final class SharedPathMonitor {
static let queue = Queue() static let queue = Queue()
static let impl = QueueLocalObject<PathMonitor>(queue: queue, generate: { static let impl = QueueLocalObject<PathMonitor>(queue: queue, generate: {
@ -149,7 +149,7 @@ public enum Reachability {
} }
public static var networkType: Signal<NetworkType, NoError> { 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 return Signal { subscriber in
let disposable = MetaDisposable() let disposable = MetaDisposable()

View File

@ -73,6 +73,8 @@ public final class CallController: ViewController {
private let idleTimerExtensionDisposable = MetaDisposable() private let idleTimerExtensionDisposable = MetaDisposable()
public var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)?
public init(sharedContext: SharedAccountContext, account: Account, call: PresentationCall, easyDebugAccess: Bool) { public init(sharedContext: SharedAccountContext, account: Account, call: PresentationCall, easyDebugAccess: Bool) {
self.sharedContext = sharedContext self.sharedContext = sharedContext
self.account = account self.account = account
@ -136,7 +138,16 @@ public final class CallController: ViewController {
override public func loadDisplayNode() { override public func loadDisplayNode() {
if self.sharedContext.immediateExperimentalUISettings.callUIV2 { 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 { } 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) 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)
} }

View File

@ -17,6 +17,7 @@ import ImageBlur
import TelegramVoip import TelegramVoip
import MetalEngine import MetalEngine
import DeviceAccess import DeviceAccess
import LibYuvBinding
final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol { final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol {
private let sharedContext: SharedAccountContext private let sharedContext: SharedAccountContext
@ -29,8 +30,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
private let callScreen: PrivateCallScreen private let callScreen: PrivateCallScreen
private var callScreenState: PrivateCallScreen.State? private var callScreenState: PrivateCallScreen.State?
private var shouldStayHiddenUntilConnection: Bool = false
private var callStartTimestamp: Double? private var callStartTimestamp: Double?
private var callState: PresentationCallState? private var callState: PresentationCallState?
@ -47,6 +46,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
var callEnded: ((Bool) -> Void)? var callEnded: ((Bool) -> Void)?
var dismissedInteractively: (() -> Void)? var dismissedInteractively: (() -> Void)?
var dismissAllTooltips: (() -> Void)? var dismissAllTooltips: (() -> Void)?
var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)?
private var emojiKey: (data: Data, resolvedKey: [String])? private var emojiKey: (data: Data, resolvedKey: [String])?
private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)? private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)?
@ -67,7 +67,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
presentationData: PresentationData, presentationData: PresentationData,
statusBar: StatusBar, statusBar: StatusBar,
debugInfo: Signal<(String, String), NoError>, debugInfo: Signal<(String, String), NoError>,
shouldStayHiddenUntilConnection: Bool = false,
easyDebugAccess: Bool, easyDebugAccess: Bool,
call: PresentationCall call: PresentationCall
) { ) {
@ -80,8 +79,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
self.containerView = UIView() self.containerView = UIView()
self.callScreen = PrivateCallScreen() self.callScreen = PrivateCallScreen()
self.shouldStayHiddenUntilConnection = shouldStayHiddenUntilConnection
super.init() super.init()
self.view.addSubview(self.containerView) self.view.addSubview(self.containerView)
@ -122,6 +119,20 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
return return
} }
self.back?() 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( self.callScreenState = PrivateCallScreen.State(
@ -130,7 +141,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
shortName: " ", shortName: " ",
avatarImage: nil, avatarImage: nil,
audioOutput: .internalSpeaker, audioOutput: .internalSpeaker,
isMicrophoneMuted: false, isLocalAudioMuted: false,
isRemoteAudioMuted: false,
localVideo: nil, localVideo: nil,
remoteVideo: nil, remoteVideo: nil,
isRemoteBatteryLow: false isRemoteBatteryLow: false
@ -145,8 +157,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
return return
} }
self.isMuted = isMuted self.isMuted = isMuted
if callScreenState.isMicrophoneMuted != isMuted { if callScreenState.isLocalAudioMuted != isMuted {
callScreenState.isMicrophoneMuted = isMuted callScreenState.isLocalAudioMuted = isMuted
self.callScreenState = callScreenState self.callScreenState = callScreenState
self.update(transition: .animated(duration: 0.3, curve: .spring)) self.update(transition: .animated(duration: 0.3, curve: .spring))
} }
@ -310,6 +322,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
case let .active(startTime, signalQuality, keyData): case let .active(startTime, signalQuality, keyData):
self.callStartTimestamp = startTime self.callStartTimestamp = startTime
var signalQuality = signalQuality
signalQuality = 4
let _ = keyData let _ = keyData
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
startTime: startTime + kCFAbsoluteTimeIntervalSince1970, startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
@ -320,7 +335,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
let _ = keyData let _ = keyData
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
startTime: startTime + kCFAbsoluteTimeIntervalSince1970, startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 0.0), signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
emojiKey: self.resolvedEmojiKey(data: keyData) emojiKey: self.resolvedEmojiKey(data: keyData)
)) ))
case .terminating, .terminated: case .terminating, .terminated:
@ -373,6 +388,13 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
callScreenState.isRemoteBatteryLow = false callScreenState.isRemoteBatteryLow = false
} }
switch callState.remoteAudioState {
case .muted:
callScreenState.isRemoteAudioMuted = true
case .active:
callScreenState.isRemoteAudioMuted = false
}
if self.callScreenState != callScreenState { if self.callScreenState != callScreenState {
self.callScreenState = callScreenState self.callScreenState = callScreenState
self.update(transition: .animated(duration: 0.35, curve: .spring)) self.update(transition: .animated(duration: 0.35, curve: .spring))
@ -393,6 +415,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
return return
} }
callScreenState.name = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) callScreenState.name = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
callScreenState.shortName = peer.compactDisplayTitle
if self.currentPeer?.smallProfileImage != peer.smallProfileImage { if self.currentPeer?.smallProfileImage != peer.smallProfileImage {
self.peerAvatarDisposable?.dispose() self.peerAvatarDisposable?.dispose()
@ -460,16 +483,14 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
self.containerView.layer.removeAnimation(forKey: "scale") self.containerView.layer.removeAnimation(forKey: "scale")
self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) 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.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.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} }
} }
}
func animateOut(completion: @escaping () -> Void) { func animateOut(completion: @escaping () -> Void) {
self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) 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.allowsGroupOpacity = true
self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
self?.containerView.layer.allowsGroupOpacity = false 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.containerView, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(view: self.callScreen, 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( self.callScreen.update(
size: layout.size, size: layout.size,
insets: layout.insets(options: [.statusBar]), insets: layout.insets(options: [.statusBar]),
interfaceOrientation: layout.metrics.orientation ?? .portrait,
screenCornerRadius: layout.deviceMetrics.screenCornerRadius, screenCornerRadius: layout.deviceMetrics.screenCornerRadius,
state: callScreenState, state: callScreenState,
transition: Transition(transition) 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 { 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 static let queue = Queue(name: "AdaptedCallVideoSource")
private var onUpdatedListeners = Bag<() -> Void>() private var onUpdatedListeners = Bag<() -> Void>()
private(set) var currentOutput: Output? private(set) var currentOutput: Output?
@ -540,6 +662,8 @@ private final class AdaptedCallVideoSource: VideoSource {
rotationAngle = Float.pi * 3.0 / 2.0 rotationAngle = Float.pi * 3.0 / 2.0
} }
let followsDeviceOrientation = videoFrameData.deviceRelativeOrientation != nil
var mirrorDirection: Output.MirrorDirection = [] var mirrorDirection: Output.MirrorDirection = []
var sourceId: Int = 0 var sourceId: Int = 0
@ -601,12 +725,45 @@ private final class AdaptedCallVideoSource: VideoSource {
output = Output( output = Output(
resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)), resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)),
textureLayout: .biPlanar(Output.BiPlanarTextureLayout(
y: yTexture, y: yTexture,
uv: uvTexture, uv: uvTexture
)),
dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer),
rotationAngle: rotationAngle, rotationAngle: rotationAngle,
followsDeviceOrientation: followsDeviceOrientation,
mirrorDirection: mirrorDirection, mirrorDirection: mirrorDirection,
sourceId: sourceId 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: default:
return return
} }

View File

@ -342,6 +342,32 @@ public final class PresentationCallImpl: PresentationCall {
} }
private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?, audioSessionControl: ManagedAudioSessionControl?) { 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 { if case .video = sessionState.type {
self.isVideo = true self.isVideo = true
} }
@ -349,9 +375,10 @@ public final class PresentationCallImpl: PresentationCall {
let previousControl = self.audioSessionControl let previousControl = self.audioSessionControl
self.sessionState = sessionState self.sessionState = sessionState
self.callContextState = callContextState self.callContextState = callContextState
self.reception = reception
self.audioSessionControl = audioSessionControl self.audioSessionControl = audioSessionControl
let reception = self.reception
if previousControl != nil && audioSessionControl == nil { if previousControl != nil && audioSessionControl == nil {
print("updateSessionState \(sessionState.state) \(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 self.audioLevelDisposable = (ongoingContext.audioLevel
|> deliverOnMainQueue).start(next: { [weak self] level in |> deliverOnMainQueue).start(next: { [weak self] level in
if let strongSelf = self { if let strongSelf = self {

View File

@ -514,7 +514,7 @@ func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializa
} }
if useNetworkFramework { if useNetworkFramework {
if #available(iOS 12.0, macOS 10.14, *) { if #available(iOS 12.0, macOS 14.0, *) {
context.makeTcpConnectionInterface = { delegate, delegateQueue in context.makeTcpConnectionInterface = { delegate, delegateQueue in
return NetworkFrameworkTcpConnectionInterface(delegate: delegate, delegateQueue: delegateQueue) return NetworkFrameworkTcpConnectionInterface(delegate: delegate, delegateQueue: delegateQueue)
} }

View File

@ -4,7 +4,7 @@ import Network
import MtProtoKit import MtProtoKit
import SwiftSignalKit import SwiftSignalKit
@available(iOS 12.0, macOS 10.14, *) @available(iOS 12.0, macOS 14.0, *)
final class NetworkFrameworkTcpConnectionInterface: NSObject, MTTcpConnectionInterface { final class NetworkFrameworkTcpConnectionInterface: NSObject, MTTcpConnectionInterface {
private struct ReadRequest { private struct ReadRequest {
let length: Int let length: Int

View File

@ -3,7 +3,11 @@ import Postbox
import SwiftSignalKit import SwiftSignalKit
import TelegramApi import TelegramApi
import MtProtoKit import MtProtoKit
import LinkPresentation
#if os(iOS)
import UIKit
#endif
import CoreServices
public enum WebpagePreviewResult: Equatable { public enum WebpagePreviewResult: Equatable {
public struct Result: Equatable { public struct Result: Equatable {
@ -14,9 +18,13 @@ public enum WebpagePreviewResult: Equatable {
case progress case progress
case result(Result?) 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 |> mapToSignal { next -> Signal<WebpagePreviewResult, NoError> in
if case let .result(result) = next { if case let .result(result) = next {
return .single(.result(result)) return .single(.result(result))
@ -35,7 +43,7 @@ public func normalizedWebpagePreviewUrl(url: String) -> String {
return url 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 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 { if let webpageId = webpageId, let webpage = transaction.getMedia(webpageId) as? TelegramMediaWebpage, let url = webpage.content.url {
var sourceUrl = 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))) return .single(.result(WebpagePreviewResult.Result(webpage: webpage, sourceUrl: sourceUrl)))
} else { } 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) 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 |> `catch` { _ -> Signal<NetworkRequestResult<Api.MessageMedia>, NoError> in
return .single(.result(.messageMediaEmpty)) return .single(.result(.messageMediaEmpty))

View File

@ -67,6 +67,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/AppBundle", "//submodules/AppBundle",
"//submodules/UIKitRuntimeUtils",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -236,7 +236,7 @@ fragment half4 callBlobFragment(
return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha); 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> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureUV [[ texture(1) ]], texture2d<half, access::read> inTextureUV [[ texture(1) ]],
texture2d<half, access::write> outTexture [[ texture(2) ]], texture2d<half, access::write> outTexture [[ texture(2) ]],
@ -249,6 +249,22 @@ kernel void videoYUVToRGBA(
outTexture.write(color, threadPosition); 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( vertex QuadVertexOut mainVideoVertex(
const device Rectangle &rect [[ buffer(0) ]], const device Rectangle &rect [[ buffer(0) ]],
const device uint2 &mirror [[ buffer(1) ]], const device uint2 &mirror [[ buffer(1) ]],

View File

@ -58,8 +58,10 @@ final class ButtonGroupView: OverlayMaskContainerView {
private var buttons: [Button]? private var buttons: [Button]?
private var buttonViews: [Button.Content.Key: ContentOverlayButton] = [:] private var buttonViews: [Button.Content.Key: ContentOverlayButton] = [:]
private var noticeViews: [AnyHashable: NoticeView] = [:] private var noticeViews: [AnyHashable: NoticeView] = [:]
private var closeButtonView: CloseButtonView?
var closePressed: (() -> Void)?
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
@ -79,7 +81,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
return result 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 self.buttons = buttons
let buttonSize: CGFloat = 56.0 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) 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 { for button in buttons {
let title: String let title: String
let image: UIImage? let image: UIImage?
@ -213,9 +255,10 @@ final class ButtonGroupView: OverlayMaskContainerView {
Transition.immediate.setScale(view: buttonView, scale: 0.001) Transition.immediate.setScale(view: buttonView, scale: 0.001)
buttonView.alpha = 0.0 buttonView.alpha = 0.0
transition.setScale(view: buttonView, scale: 1.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))) 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) buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: buttonTransition)
buttonX += buttonSize + buttonSpacing buttonX += buttonSize + buttonSpacing

View File

@ -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)
}
}

View File

@ -63,8 +63,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
if highlighted { if highlighted {
self.layer.removeAnimation(forKey: "opacity") self.layer.removeAnimation(forKey: "sublayerTransform")
self.layer.removeAnimation(forKey: "transform")
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
transition.setScale(layer: self.layer, scale: topScale) transition.setScale(layer: self.layer, scale: topScale)
} else { } else {

View File

@ -5,10 +5,10 @@ import ComponentFlow
final class EmojiExpandedInfoView: OverlayMaskContainerView { final class EmojiExpandedInfoView: OverlayMaskContainerView {
private struct Params: Equatable { private struct Params: Equatable {
var constrainedWidth: CGFloat var width: CGFloat
init(constrainedWidth: CGFloat) { init(width: CGFloat) {
self.constrainedWidth = constrainedWidth self.width = width
} }
} }
@ -129,8 +129,8 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView {
return nil return nil
} }
func update(constrainedWidth: CGFloat, transition: Transition) -> CGSize { func update(width: CGFloat, transition: Transition) -> CGSize {
let params = Params(constrainedWidth: constrainedWidth) let params = Params(width: width)
if let currentLayout = self.currentLayout, currentLayout.params == params { if let currentLayout = self.currentLayout, currentLayout.params == params {
return currentLayout.size return currentLayout.size
} }
@ -142,16 +142,12 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView {
private func update(params: Params, transition: Transition) -> CGSize { private func update(params: Params, transition: Transition) -> CGSize {
let buttonHeight: CGFloat = 56.0 let buttonHeight: CGFloat = 56.0
var constrainedWidth = params.constrainedWidth 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)
constrainedWidth = min(constrainedWidth, 300.0) 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 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)) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))

View File

@ -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)
}
}

View File

@ -11,7 +11,8 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
let blurredLayer: MetalEngineSubjectLayer let blurredLayer: MetalEngineSubjectLayer
final class BlurState: ComputeState { final class BlurState: ComputeState {
let computePipelineStateYUVToRGBA: MTLComputePipelineState let computePipelineStateYUVBiPlanarToRGBA: MTLComputePipelineState
let computePipelineStateYUVTriPlanarToRGBA: MTLComputePipelineState
let computePipelineStateHorizontal: MTLComputePipelineState let computePipelineStateHorizontal: MTLComputePipelineState
let computePipelineStateVertical: MTLComputePipelineState let computePipelineStateVertical: MTLComputePipelineState
let downscaleKernel: MPSImageBilinearScale let downscaleKernel: MPSImageBilinearScale
@ -20,13 +21,22 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
guard let library = metalLibrary(device: device) else { guard let library = metalLibrary(device: device) else {
return nil return nil
} }
guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else {
guard let functionVideoBiPlanarToRGBA = library.makeFunction(name: "videoBiPlanarToRGBA") else {
return nil return nil
} }
guard let computePipelineStateYUVToRGBA = try? device.makeComputePipelineState(function: functionVideoYUVToRGBA) else { guard let computePipelineStateYUVBiPlanarToRGBA = try? device.makeComputePipelineState(function: functionVideoBiPlanarToRGBA) else {
return nil 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 { guard let gaussianBlurHorizontal = library.makeFunction(name: "gaussianBlurHorizontal"), let gaussianBlurVertical = library.makeFunction(name: "gaussianBlurVertical") else {
return nil return nil
@ -107,7 +117,7 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
return 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 { if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec {
self.rgbaTexture = MetalEngine.shared.pooledTexture(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 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) 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) switch videoTextures.textureLayout {
computeEncoder.setTexture(videoTextures.y, index: 0) case let .biPlanar(biPlanar):
computeEncoder.setTexture(videoTextures.uv, index: 1) computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVBiPlanarToRGBA)
computeEncoder.setTexture(biPlanar.y, index: 0)
computeEncoder.setTexture(biPlanar.uv, index: 1)
computeEncoder.setTexture(rgbaTexture, index: 2) 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.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding() computeEncoder.endEncoding()
@ -198,8 +217,8 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
encoder.setFragmentTexture(blurredTexture, index: 0) encoder.setFragmentTexture(blurredTexture, index: 0)
var brightness: Float = 1.0 var brightness: Float = 0.7
var saturation: Float = 1.2 var saturation: Float = 1.3
var overlay: SIMD4<Float> = SIMD4<Float>(1.0, 1.0, 1.0, 0.2) var overlay: SIMD4<Float> = SIMD4<Float>(1.0, 1.0, 1.0, 0.2)
encoder.setFragmentBytes(&brightness, length: 4, index: 0) encoder.setFragmentBytes(&brightness, length: 4, index: 0)
encoder.setFragmentBytes(&saturation, length: 4, index: 1) encoder.setFragmentBytes(&saturation, length: 4, index: 1)

View File

@ -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
}
}

View File

@ -5,17 +5,16 @@ import ComponentFlow
final class RoundedCornersView: UIImageView { final class RoundedCornersView: UIImageView {
private let color: UIColor private let color: UIColor
private let smoothCorners: Bool
private var currentCornerRadius: CGFloat? private var currentCornerRadius: CGFloat?
private var cornerImage: UIImage? private var cornerImage: UIImage?
init(color: UIColor) { init(color: UIColor, smoothCorners: Bool = false) {
self.color = color self.color = color
self.smoothCorners = smoothCorners
super.init(image: nil) super.init(image: nil)
if #available(iOS 13.0, *) {
self.layer.cornerCurve = .circular
}
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -26,11 +25,34 @@ final class RoundedCornersView: UIImageView {
guard let cornerRadius = self.currentCornerRadius else { guard let cornerRadius = self.currentCornerRadius else {
return 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 {
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 { } else {
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0) 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.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color)
} }
}
}
self.image = self.cornerImage self.image = self.cornerImage
self.clipsToBounds = false self.clipsToBounds = false
self.backgroundColor = nil self.backgroundColor = nil
@ -52,6 +74,14 @@ final class RoundedCornersView: UIImageView {
if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil { if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil {
self.layer.cornerRadius = previousCornerRadius 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 transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in
guard let self, completed else { guard let self, completed else {
return return

View File

@ -9,6 +9,26 @@ private let shadowImage: UIImage? = {
UIImage(named: "Call/VideoGradient")?.precomposed() 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 { private final class VideoContainerLayer: SimpleLayer {
let contentsLayer: SimpleLayer let contentsLayer: SimpleLayer
@ -44,14 +64,16 @@ final class VideoContainerView: HighlightTrackingButton {
private struct Params: Equatable { private struct Params: Equatable {
var size: CGSize var size: CGSize
var insets: UIEdgeInsets var insets: UIEdgeInsets
var interfaceOrientation: UIInterfaceOrientation
var cornerRadius: CGFloat var cornerRadius: CGFloat
var controlsHidden: Bool var controlsHidden: Bool
var isMinimized: Bool var isMinimized: Bool
var isAnimatedOut: 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.size = size
self.insets = insets self.insets = insets
self.interfaceOrientation = interfaceOrientation
self.cornerRadius = cornerRadius self.cornerRadius = cornerRadius
self.controlsHidden = controlsHidden self.controlsHidden = controlsHidden
self.isMinimized = isMinimized self.isMinimized = isMinimized
@ -59,14 +81,16 @@ final class VideoContainerView: HighlightTrackingButton {
} }
} }
private struct VideoMetrics: Equatable { struct VideoMetrics: Equatable {
var resolution: CGSize var resolution: CGSize
var rotationAngle: Float var rotationAngle: Float
var followsDeviceOrientation: Bool
var sourceId: Int var sourceId: Int
init(resolution: CGSize, rotationAngle: Float, sourceId: Int) { init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool, sourceId: Int) {
self.resolution = resolution self.resolution = resolution
self.rotationAngle = rotationAngle self.rotationAngle = rotationAngle
self.followsDeviceOrientation = followsDeviceOrientation
self.sourceId = sourceId self.sourceId = sourceId
} }
} }
@ -74,10 +98,12 @@ final class VideoContainerView: HighlightTrackingButton {
private final class FlipAnimationInfo { private final class FlipAnimationInfo {
let isForward: Bool let isForward: Bool
let previousRotationAngle: Float let previousRotationAngle: Float
let followsDeviceOrientation: Bool
init(isForward: Bool, previousRotationAngle: Float) { init(isForward: Bool, previousRotationAngle: Float, followsDeviceOrientation: Bool) {
self.isForward = isForward self.isForward = isForward
self.previousRotationAngle = previousRotationAngle self.previousRotationAngle = previousRotationAngle
self.followsDeviceOrientation = followsDeviceOrientation
} }
} }
@ -141,11 +167,11 @@ final class VideoContainerView: HighlightTrackingButton {
var videoMetrics: VideoMetrics? var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput { if let currentOutput = self.video?.currentOutput {
if let previousVideo = self.videoLayer.video, previousVideo.sourceId != currentOutput.sourceId { 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 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 { } else {
self.videoLayer.video = nil self.videoLayer.video = nil
} }
@ -164,7 +190,7 @@ final class VideoContainerView: HighlightTrackingButton {
var videoMetrics: VideoMetrics? var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput { if let currentOutput = self.video?.currentOutput {
self.videoLayer.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 { } else {
self.videoLayer.video = nil self.videoLayer.video = nil
} }
@ -382,7 +408,7 @@ final class VideoContainerView: HighlightTrackingButton {
self.dragPositionAnimatorLink = nil self.dragPositionAnimatorLink = nil
return 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 let targetPosition = videoLayout.rotatedVideoFrame.center
self.dragVelocity = self.updateVelocityUsingSpring( self.dragVelocity = self.updateVelocityUsingSpring(
@ -443,8 +469,8 @@ final class VideoContainerView: HighlightTrackingButton {
self.update(previousParams: params, params: params, transition: transition) self.update(previousParams: params, params: params, transition: transition)
} }
func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) { 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, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut) let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut)
if self.params == params { if self.params == params {
return return
} }
@ -469,10 +495,10 @@ final class VideoContainerView: HighlightTrackingButton {
var effectiveVideoFrame: CGRect 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 rotatedResolution = videoMetrics.resolution
var videoIsRotated = false 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) rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
videoIsRotated = true videoIsRotated = true
} }
@ -505,7 +531,7 @@ final class VideoContainerView: HighlightTrackingButton {
var videoTransform = CATransform3DIdentity var videoTransform = CATransform3DIdentity
videoTransform.m34 = 1.0 / 600.0 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 { if params.isAnimatedOut {
videoTransform = CATransform3DScale(videoTransform, 0.6, 0.6, 1.0) videoTransform = CATransform3DScale(videoTransform, 0.6, 0.6, 1.0)
} }
@ -530,10 +556,12 @@ final class VideoContainerView: HighlightTrackingButton {
} }
self.appliedVideoMetrics = videoMetrics self.appliedVideoMetrics = videoMetrics
let resolvedRotationAngle = resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation)
if params.isMinimized { if params.isMinimized {
self.isFillingBounds = false 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) transition.setPosition(layer: self.videoContainerLayer, position: videoLayout.rotatedVideoFrame.center)
@ -558,23 +586,25 @@ final class VideoContainerView: HighlightTrackingButton {
if let disappearingVideoLayer = self.disappearingVideoLayer { if let disappearingVideoLayer = self.disappearingVideoLayer {
self.disappearingVideoLayer = nil 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 let initialDisapparingVideoSize = disappearingVideoLayout.rotatedVideoSize
if !disappearingVideoLayer.isAlphaAnimationInitiated { if !disappearingVideoLayer.isAlphaAnimationInitiated {
disappearingVideoLayer.isAlphaAnimationInitiated = true disappearingVideoLayer.isAlphaAnimationInitiated = true
if let flipAnimationInfo = disappearingVideoLayer.flipAnimationInfo { if let flipAnimationInfo = disappearingVideoLayer.flipAnimationInfo {
let resolvedPreviousRotationAngle = resolveVideoRotationAngle(angle: flipAnimationInfo.previousRotationAngle, followsDeviceOrientation: flipAnimationInfo.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation)
var videoTransform = self.videoContainerLayer.transform var videoTransform = self.videoContainerLayer.transform
var axis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0.0, 0.0, 0.0) var axis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0.0, 0.0, 0.0)
let previousVideoScale: CGPoint let previousVideoScale: CGPoint
if flipAnimationInfo.previousRotationAngle == Float.pi * 0.5 { if resolvedPreviousRotationAngle == Float.pi * 0.5 {
axis.x = -1.0 axis.x = -1.0
previousVideoScale = CGPoint(x: 1.0, y: -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 axis.y = -1.0
previousVideoScale = CGPoint(x: -1.0, 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 axis.x = 1.0
previousVideoScale = CGPoint(x: 1.0, y: 1.0) previousVideoScale = CGPoint(x: 1.0, y: 1.0)
} else { } else {
@ -652,7 +682,7 @@ final class VideoContainerView: HighlightTrackingButton {
} else { } else {
var rotatedResolution = videoMetrics.resolution var rotatedResolution = videoMetrics.resolution
var videoIsRotated = false 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) rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
videoIsRotated = true 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.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.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
videoTransition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size)) videoTransition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
videoTransition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 1.0) 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 { if !params.isAnimatedOut {
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2) self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)

View File

@ -16,18 +16,67 @@ public final class VideoSourceOutput {
public static let vertical = MirrorDirection(rawValue: 1 << 1) public static let vertical = MirrorDirection(rawValue: 1 << 1)
} }
public let resolution: CGSize open class DataBuffer {
open var pixelBuffer: CVPixelBuffer? {
return nil
}
public init() {
}
}
public final class BiPlanarTextureLayout {
public let y: MTLTexture public let y: MTLTexture
public let uv: 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 textureLayout: TextureLayout
public let dataBuffer: DataBuffer
public let rotationAngle: Float public let rotationAngle: Float
public let followsDeviceOrientation: Bool
public let mirrorDirection: MirrorDirection public let mirrorDirection: MirrorDirection
public let sourceId: Int 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.resolution = resolution
self.y = y self.textureLayout = textureLayout
self.uv = uv self.dataBuffer = dataBuffer
self.rotationAngle = rotationAngle self.rotationAngle = rotationAngle
self.followsDeviceOrientation = followsDeviceOrientation
self.mirrorDirection = mirrorDirection self.mirrorDirection = mirrorDirection
self.sourceId = sourceId self.sourceId = sourceId
} }
@ -161,7 +210,18 @@ public final class FileVideoSource: VideoSource {
resolution.width = floor(resolution.width * self.sizeMultiplicator.x) resolution.width = floor(resolution.width * self.sizeMultiplicator.x)
resolution.height = floor(resolution.height * self.sizeMultiplicator.y) 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 return true
} }
} }

View File

@ -1,58 +1,14 @@
import Foundation import Foundation
import AVFoundation
import AVKit
import UIKit import UIKit
import Display import Display
import MetalEngine import MetalEngine
import ComponentFlow import ComponentFlow
import SwiftSignalKit import SwiftSignalKit
import UIKitRuntimeUtils
/*private final class EdgeTestLayer: MetalEngineSubjectLayer, MetalEngineSubject { public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictureControllerDelegate {
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 struct State: Equatable { public struct State: Equatable {
public struct SignalInfo: Equatable { public struct SignalInfo: Equatable {
public var quality: Double public var quality: Double
@ -100,7 +56,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
public var shortName: String public var shortName: String
public var avatarImage: UIImage? public var avatarImage: UIImage?
public var audioOutput: AudioOutput public var audioOutput: AudioOutput
public var isMicrophoneMuted: Bool public var isLocalAudioMuted: Bool
public var isRemoteAudioMuted: Bool
public var localVideo: VideoSource? public var localVideo: VideoSource?
public var remoteVideo: VideoSource? public var remoteVideo: VideoSource?
public var isRemoteBatteryLow: Bool public var isRemoteBatteryLow: Bool
@ -111,7 +68,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
shortName: String, shortName: String,
avatarImage: UIImage?, avatarImage: UIImage?,
audioOutput: AudioOutput, audioOutput: AudioOutput,
isMicrophoneMuted: Bool, isLocalAudioMuted: Bool,
isRemoteAudioMuted: Bool,
localVideo: VideoSource?, localVideo: VideoSource?,
remoteVideo: VideoSource?, remoteVideo: VideoSource?,
isRemoteBatteryLow: Bool isRemoteBatteryLow: Bool
@ -121,7 +79,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.shortName = shortName self.shortName = shortName
self.avatarImage = avatarImage self.avatarImage = avatarImage
self.audioOutput = audioOutput self.audioOutput = audioOutput
self.isMicrophoneMuted = isMicrophoneMuted self.isLocalAudioMuted = isLocalAudioMuted
self.isRemoteAudioMuted = isRemoteAudioMuted
self.localVideo = localVideo self.localVideo = localVideo
self.remoteVideo = remoteVideo self.remoteVideo = remoteVideo
self.isRemoteBatteryLow = isRemoteBatteryLow self.isRemoteBatteryLow = isRemoteBatteryLow
@ -143,7 +102,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
if lhs.audioOutput != rhs.audioOutput { if lhs.audioOutput != rhs.audioOutput {
return false return false
} }
if lhs.isMicrophoneMuted != rhs.isMicrophoneMuted { if lhs.isLocalAudioMuted != rhs.isLocalAudioMuted {
return false
}
if lhs.isRemoteAudioMuted != rhs.isRemoteAudioMuted {
return false return false
} }
if lhs.localVideo !== rhs.localVideo { if lhs.localVideo !== rhs.localVideo {
@ -162,12 +124,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
private struct Params: Equatable { private struct Params: Equatable {
var size: CGSize var size: CGSize
var insets: UIEdgeInsets var insets: UIEdgeInsets
var interfaceOrientation: UIInterfaceOrientation
var screenCornerRadius: CGFloat var screenCornerRadius: CGFloat
var state: State 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.size = size
self.insets = insets self.insets = insets
self.interfaceOrientation = interfaceOrientation
self.screenCornerRadius = screenCornerRadius self.screenCornerRadius = screenCornerRadius
self.state = state self.state = state
} }
@ -204,11 +168,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
private var activeLocalVideoSource: VideoSource? private var activeLocalVideoSource: VideoSource?
private var waitingForFirstLocalVideoFrameDisposable: Disposable? private var waitingForFirstLocalVideoFrameDisposable: Disposable?
private var isUpdating: Bool = false
private var canAnimateAudioLevel: Bool = false private var canAnimateAudioLevel: Bool = false
private var displayEmojiTooltip: Bool = false private var displayEmojiTooltip: Bool = false
private var isEmojiKeyExpanded: Bool = false private var isEmojiKeyExpanded: Bool = false
private var areControlsHidden: Bool = false private var areControlsHidden: Bool = false
private var swapLocalAndRemoteVideo: Bool = false private var swapLocalAndRemoteVideo: Bool = false
private var isPictureInPictureActive: Bool = false
private var processedInitialAudioLevelBump: Bool = false private var processedInitialAudioLevelBump: Bool = false
private var audioLevelBump: Float = 0.0 private var audioLevelBump: Float = 0.0
@ -224,6 +191,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
public var microhoneMuteAction: (() -> Void)? public var microhoneMuteAction: (() -> Void)?
public var endCallAction: (() -> Void)? public var endCallAction: (() -> Void)?
public var backAction: (() -> 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) { public override init(frame: CGRect) {
self.overlayContentsView = UIView() self.overlayContentsView = UIView()
@ -249,6 +223,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.backButtonView = BackButtonView(text: "Back") self.backButtonView = BackButtonView(text: "Back")
self.pipView = PrivateCallPictureInPictureView(frame: CGRect(origin: CGPoint(), size: CGSize()))
super.init(frame: frame) super.init(frame: frame)
self.clipsToBounds = true self.clipsToBounds = true
@ -264,10 +240,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.avatarTransformLayer.addSublayer(self.avatarLayer) self.avatarTransformLayer.addSublayer(self.avatarLayer)
self.layer.addSublayer(self.avatarTransformLayer) 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.addSubview(self.videoContainerBackgroundView)
self.overlayContentsView.mask = self.maskContents self.overlayContentsView.mask = self.maskContents
@ -310,6 +282,27 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
} }
self.backAction?() 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) { public required init?(coder: NSCoder) {
@ -335,6 +328,39 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
return result 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) { public func addIncomingAudioLevel(value: Float) {
if self.canAnimateAudioLevel { if self.canAnimateAudioLevel {
self.targetAudioLevel = value 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) { public func beginPictureInPictureIfPossible() {
let params = Params(size: size, insets: insets, screenCornerRadius: screenCornerRadius, state: state) 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 { if self.params == params {
return return
} }
@ -487,6 +519,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
} }
private func updateInternal(params: Params, transition: Transition) { private func updateInternal(params: Params, transition: Transition) {
self.isUpdating = true
defer {
self.isUpdating = false
}
let genericAlphaTransition: Transition let genericAlphaTransition: Transition
switch transition.animation { switch transition.animation {
case .none: case .none:
@ -497,6 +534,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
let backgroundFrame = CGRect(origin: CGPoint(), size: params.size) 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)] = [] var activeVideoSources: [(VideoContainerView.Key, VideoSource)] = []
if self.swapLocalAndRemoteVideo { if self.swapLocalAndRemoteVideo {
if let activeLocalVideoSource = self.activeLocalVideoSource { if let activeLocalVideoSource = self.activeLocalVideoSource {
@ -515,6 +559,41 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
} }
let havePrimaryVideo = !activeVideoSources.isEmpty 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 currentAreControlsHidden = havePrimaryVideo && self.areControlsHidden
let backgroundAspect: CGFloat = params.size.width / params.size.height let backgroundAspect: CGFloat = params.size.width / params.size.height
@ -554,7 +633,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
} }
self.videoAction?() 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 { guard let self else {
return return
} }
@ -584,17 +663,24 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
} }
var notices: [ButtonGroupView.Notice] = [] 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")) 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 { 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 { 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? var expandedEmojiKeyRect: CGRect?
if self.isEmojiKeyExpanded { 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) 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.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)) 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.position = self.avatarTransformLayer.position
videoContainerView.blurredContainerLayer.bounds = self.avatarTransformLayer.bounds videoContainerView.blurredContainerLayer.bounds = self.avatarTransformLayer.bounds
videoContainerView.blurredContainerLayer.opacity = 0.0 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: videoContainerView, scale: self.currentAvatarAudioScale)
Transition.immediate.setScale(view: self.videoContainerBackgroundView, scale: self.currentAvatarAudioScale) Transition.immediate.setScale(view: self.videoContainerBackgroundView, scale: self.currentAvatarAudioScale)
} else { } else {
@ -865,7 +951,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
videoContainerView.blurredContainerLayer.position = expandedVideoFrame.center videoContainerView.blurredContainerLayer.position = expandedVideoFrame.center
videoContainerView.blurredContainerLayer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size) videoContainerView.blurredContainerLayer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size)
videoContainerView.blurredContainerLayer.opacity = 0.0 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.setPosition(layer: videoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
videoContainerTransition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size)) videoContainerTransition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
videoContainerTransition.setScale(layer: videoContainerView.blurredContainerLayer, scale: 1.0) 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 let alphaTransition: Transition
switch transition.animation { switch transition.animation {
@ -897,8 +983,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
} }
} }
alphaTransition.setAlpha(view: videoContainerView, alpha: 1.0) let videoAlpha: CGFloat = self.isPictureInPictureActive ? 0.0 : 1.0
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0) alphaTransition.setAlpha(view: videoContainerView, alpha: videoAlpha)
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: videoAlpha)
} }
var removedVideoContainerIndices: [Int] = [] var removedVideoContainerIndices: [Int] = []
@ -910,7 +997,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
if self.videoContainerViews.count == 1 || (i == 0 && !havePrimaryVideo) { if self.videoContainerViews.count == 1 || (i == 0 && !havePrimaryVideo) {
let alphaTransition: Transition = genericAlphaTransition 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.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center)
transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0) 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) 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)
} }
} }
} }

View File

@ -71,6 +71,8 @@ private final class AnimatableProperty<T: Interpolatable> {
let timeFromStart = timestamp - animation.startTimestamp let timeFromStart = timestamp - animation.startTimestamp
var t = max(0.0, timeFromStart / duration) var t = max(0.0, timeFromStart / duration)
switch curve { switch curve {
case .linear:
break
case .easeInOut: case .easeInOut:
t = listViewAnimationCurveEaseInOut(t) t = listViewAnimationCurveEaseInOut(t)
case .spring: case .spring:

View File

@ -2455,7 +2455,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
inputPanelHeight: 0.0, inputPanelHeight: 0.0,
transition: .immediate, transition: .immediate,
interfaceState: presentationInterfaceState, interfaceState: presentationInterfaceState,
layoutMetrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), layoutMetrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil),
deviceMetrics: DeviceMetrics.iPhone12, deviceMetrics: DeviceMetrics.iPhone12,
isVisible: true, isVisible: true,
isExpanded: false isExpanded: false

View File

@ -70,7 +70,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
public var timerUpdated: ((NSNumber?) -> Void)? public var timerUpdated: ((NSNumber?) -> Void)?
public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { 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 { public func caption() -> NSAttributedString {

View File

@ -97,6 +97,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
let contextSourceNode: ContextReferenceContentNode let contextSourceNode: ContextReferenceContentNode
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let backIconLayer: SimpleShapeLayer
private var animationNode: MoreIconNode? private var animationNode: MoreIconNode?
private let backgroundNode: NavigationBackgroundNode private let backgroundNode: NavigationBackgroundNode
@ -117,6 +118,15 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true 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) self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: true)
super.init(pointerStyle: .insetRectangle(-8.0, 2.0)) 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.backgroundNode)
self.contextSourceNode.addSubnode(self.textNode) self.contextSourceNode.addSubnode(self.textNode)
self.contextSourceNode.addSubnode(self.iconNode) self.contextSourceNode.addSubnode(self.iconNode)
self.contextSourceNode.layer.addSublayer(self.backIconLayer)
self.addSubnode(self.containerNode) self.addSubnode(self.containerNode)
@ -146,13 +157,43 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
self.action?(self.contextSourceNode, nil) 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.contentsColor = contentsColor
self.backgroundNode.updateColor(color: backgroundColor, transition: transition) self.backgroundNode.updateColor(color: backgroundColor, transition: transition)
transition.updateTintColor(layer: self.textNode.layer, color: self.contentsColor) transition.updateTintColor(layer: self.textNode.layer, color: self.contentsColor)
transition.updateTintColor(layer: self.iconNode.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 { if let animationNode = self.animationNode {
transition.updateTintColor(layer: animationNode.imageNode.layer, color: self.contentsColor) transition.updateTintColor(layer: animationNode.imageNode.layer, color: self.contentsColor)
@ -184,9 +225,9 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
var animationState: MoreIconNodeState = .more var animationState: MoreIconNodeState = .more
switch key { switch key {
case .back: case .back:
text = "" text = presentationData.strings.Common_Back
accessibilityText = presentationData.strings.Common_Back accessibilityText = presentationData.strings.Common_Back
icon = NavigationBar.thinBackArrowImage icon = NavigationBar.backArrowImage(color: .white)
case .edit: case .edit:
text = presentationData.strings.Common_Edit text = presentationData.strings.Common_Edit
accessibilityText = text accessibilityText = text
@ -270,11 +311,19 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
} }
let inset: CGFloat = 0.0 let inset: CGFloat = 0.0
var textInset: CGFloat = 0.0
switch key {
case .back:
textInset += 11.0
default:
break
}
let resultSize: CGSize let resultSize: CGSize
let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize) let textFrame = CGRect(origin: CGPoint(x: inset + textInset, y: floor((height - textSize.height) / 2.0)), size: textSize)
self.textNode.frame = textFrame self.textNode.position = textFrame.center
self.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
if let animationNode = self.animationNode { if let animationNode = self.animationNode {
let animationSize = CGSize(width: 30.0, height: 30.0) 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) self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size)
resultSize = size resultSize = size
} else if let image = self.iconNode.image { } 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) let size = CGSize(width: image.size.width + inset * 2.0, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: size) self.containerNode.frame = CGRect(origin: CGPoint(), size: size)

View File

@ -36,18 +36,21 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode {
private var backgroundContentColor: UIColor = .clear private var backgroundContentColor: UIColor = .clear
private var contentsColor: UIColor = .white private var contentsColor: UIColor = .white
private var canBeExpanded: Bool = false
var performAction: ((PeerInfoHeaderNavigationButtonKey, ContextReferenceContentNode?, ContextGesture?) -> Void)? 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.backgroundContentColor = backgroundContentColor
self.contentsColor = contentsColor self.contentsColor = contentsColor
for (_, button) in self.leftButtonNodes { 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 { 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.frame = buttonFrame
buttonNode.alpha = 0.0 buttonNode.alpha = 0.0
transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) 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 { } else {
transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
@ -202,7 +205,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode {
} }
let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction)
if wasAdded { 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 { if key == .moreToSearch {
buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)

View File

@ -553,6 +553,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let navigationContentsAccentColor: UIColor let navigationContentsAccentColor: UIColor
let navigationContentsPrimaryColor: UIColor let navigationContentsPrimaryColor: UIColor
let navigationContentsSecondaryColor: UIColor let navigationContentsSecondaryColor: UIColor
let navigationContentsCanBeExpanded: Bool
let contentButtonBackgroundColor: UIColor let contentButtonBackgroundColor: UIColor
let contentButtonForegroundColor: UIColor let contentButtonForegroundColor: UIColor
@ -640,6 +641,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
navigationContentsAccentColor = collapsedHeaderNavigationContentsAccentColor navigationContentsAccentColor = collapsedHeaderNavigationContentsAccentColor
navigationContentsPrimaryColor = collapsedHeaderNavigationContentsPrimaryColor navigationContentsPrimaryColor = collapsedHeaderNavigationContentsPrimaryColor
navigationContentsSecondaryColor = collapsedHeaderNavigationContentsSecondaryColor navigationContentsSecondaryColor = collapsedHeaderNavigationContentsSecondaryColor
navigationContentsCanBeExpanded = true
contentButtonBackgroundColor = collapsedHeaderContentButtonBackgroundColor contentButtonBackgroundColor = collapsedHeaderContentButtonBackgroundColor
contentButtonForegroundColor = collapsedHeaderContentButtonForegroundColor contentButtonForegroundColor = collapsedHeaderContentButtonForegroundColor
@ -651,6 +654,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
contentButtonBackgroundColor = expandedAvatarContentButtonBackgroundColor contentButtonBackgroundColor = expandedAvatarContentButtonBackgroundColor
contentButtonForegroundColor = expandedAvatarContentButtonForegroundColor contentButtonForegroundColor = expandedAvatarContentButtonForegroundColor
navigationContentsCanBeExpanded = false
headerButtonBackgroundColor = expandedAvatarHeaderButtonBackgroundColor headerButtonBackgroundColor = expandedAvatarHeaderButtonBackgroundColor
} else { } else {
let effectiveTransitionFraction: CGFloat = innerBackgroundTransitionFraction < 0.5 ? 0.0 : 1.0 let effectiveTransitionFraction: CGFloat = innerBackgroundTransitionFraction < 0.5 ? 0.0 : 1.0
@ -659,6 +664,12 @@ final class PeerInfoHeaderNode: ASDisplayNode {
navigationContentsPrimaryColor = regularNavigationContentsPrimaryColor.mixedWith(collapsedHeaderNavigationContentsPrimaryColor, alpha: effectiveTransitionFraction) navigationContentsPrimaryColor = regularNavigationContentsPrimaryColor.mixedWith(collapsedHeaderNavigationContentsPrimaryColor, alpha: effectiveTransitionFraction)
navigationContentsSecondaryColor = regularNavigationContentsSecondaryColor.mixedWith(collapsedHeaderNavigationContentsSecondaryColor, 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) contentButtonBackgroundColor = regularContentButtonBackgroundColor//.mixedWith(collapsedHeaderContentButtonBackgroundColor, alpha: effectiveTransitionFraction)
contentButtonForegroundColor = regularContentButtonForegroundColor//.mixedWith(collapsedHeaderContentButtonForegroundColor, alpha: effectiveTransitionFraction) contentButtonForegroundColor = regularContentButtonForegroundColor//.mixedWith(collapsedHeaderContentButtonForegroundColor, alpha: effectiveTransitionFraction)
@ -775,7 +786,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.titleExpandedCredibilityIconSize = expandedIconSize 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.titleNode.updateTintColor(color: navigationContentsPrimaryColor, transition: navigationTransition)
self.subtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition) self.subtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition)

View File

@ -844,7 +844,7 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
return 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] { if let webpage = webpageCache[detectedUrl] {
progress?.set(.single(false)) progress?.set(.single(false))

View File

@ -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() selfController.urlPreviewQueryState?.1.dispose()
var inScope = true var inScope = true
var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)? var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)?
@ -301,7 +301,7 @@ func updateChatPresentationInterfaceStateImpl(
let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext
let editingUrlPreviewText: NSAttributedString? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText 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() selfController.editingUrlPreviewQueryState?.1.dispose()
var inScope = true var inScope = true
var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)? var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)?

View File

@ -510,7 +510,7 @@ struct UrlPreviewState {
var detectedUrls: [String] 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 { guard let _ = inputText else {
if currentQuery != nil { if currentQuery != nil {
return (nil, .single({ _ in return nil })) return (nil, .single({ _ in return nil }))
@ -522,7 +522,7 @@ func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: Acco
let detectedUrls = detectUrls(inputText) let detectedUrls = detectUrls(inputText)
if detectedUrls != (currentQuery?.detectedUrls ?? []) { if detectedUrls != (currentQuery?.detectedUrls ?? []) {
if !detectedUrls.isEmpty { 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 |> mapToSignal { result -> Signal<(TelegramMediaWebpage, String)?, NoError> in
guard case let .result(webpageResult) = result else { guard case let .result(webpageResult) = result else {
return .complete() return .complete()

View File

@ -132,8 +132,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
private var groupCallDisposable: Disposable? private var groupCallDisposable: Disposable?
private var callController: CallController? private var callController: CallController?
private var call: PresentationCall?
public let hasOngoingCall = ValuePromise<Bool>(false) public let hasOngoingCall = ValuePromise<Bool>(false)
private let callState = Promise<PresentationCallState?>(nil) private let callState = Promise<PresentationCallState?>(nil)
private var awaitingCallConnectionDisposable: Disposable?
private var groupCallController: VoiceChatController? private var groupCallController: VoiceChatController?
public var currentGroupCallController: ViewController? { public var currentGroupCallController: ViewController? {
@ -741,26 +743,49 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.callDisposable = (callManager.currentCallSignal self.callDisposable = (callManager.currentCallSignal
|> deliverOnMainQueue).start(next: { [weak self] call in |> deliverOnMainQueue).start(next: { [weak self] call in
if let strongSelf = self { guard let self else {
if call !== strongSelf.callController?.call { return
strongSelf.callController?.dismiss()
strongSelf.callController = nil
strongSelf.hasOngoingCall.set(false)
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 !== 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 !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.callDisposable?.dispose()
self.groupCallDisposable?.dispose() self.groupCallDisposable?.dispose()
self.callStateDisposable?.dispose() self.callStateDisposable?.dispose()
self.awaitingCallConnectionDisposable?.dispose()
} }
private var didPerformAccountSettingsImport = false 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() { public func updateNotificationTokensRegistration() {
let sandbox: Bool let sandbox: Bool
#if DEBUG #if DEBUG

@ -1 +1 @@
Subproject commit fa2e53f5da9b9653ab47169a922fb6c82847134a Subproject commit 6b73742cdc140c46a1ab1b8e3390354a9738e429

View File

@ -5,6 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "LokiRng", name: "LokiRng",
platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,6 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "ShelfPack", name: "ShelfPack",
platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(
@ -23,5 +24,6 @@ let package = Package(
dependencies: [], dependencies: [],
path: ".", path: ".",
publicHeadersPath: "PublicHeaders"), publicHeadersPath: "PublicHeaders"),
] ],
cxxLanguageStandard: .cxx20
) )