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.prod",
"merchant.org.telegram.portmone.test",
"merchant.org.telegram.portmone.prod",
"merchant.org.telegram.ecommpay.test",
]

View File

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

View File

@ -507,7 +507,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
guard let presentationInterfaceState = self.presentationInterfaceState else {
return 0.0
}
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false)
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
}
public func setCaption(_ caption: NSAttributedString?) {

View File

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

View File

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

View File

@ -1776,7 +1776,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega
}
contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight))
if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
contentUnscaledSize = preferredSize
}
} else {
@ -1786,7 +1786,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega
let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset
contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(568.0, proposedContentHeight))
if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
contentUnscaledSize = preferredSize
}
}

View File

@ -183,7 +183,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.controller.containerLayoutUpdated(
ContainerViewLayout(
size: size,
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact),
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil),
deviceMetrics: parentLayout.deviceMetrics,
intrinsicInsets: UIEdgeInsets(),
safeInsets: UIEdgeInsets(),
@ -765,7 +765,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
defaultContentSize.height = min(defaultContentSize.height, 460.0)
let contentSize: CGSize
if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
contentSize = preferredSize
} else if let storedContentHeight = contentNode.storedContentHeight {
contentSize = CGSize(width: defaultContentSize.width, height: storedContentHeight)

View File

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

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) {
switch self {
case .immediate:

View File

@ -23,15 +23,18 @@ public enum ContainerViewLayoutSizeClass {
public struct LayoutMetrics: Equatable {
public let widthClass: ContainerViewLayoutSizeClass
public let heightClass: ContainerViewLayoutSizeClass
public let orientation: UIInterfaceOrientation?
public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass) {
public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass, orientation: UIInterfaceOrientation?) {
self.widthClass = widthClass
self.heightClass = heightClass
self.orientation = orientation
}
public init() {
self.widthClass = .compact
self.heightClass = .compact
self.orientation = nil
}
}

View File

@ -22,7 +22,7 @@ public final class ContextContentContainerNode: ASDisplayNode {
transition.updateBounds(node: controller, bounds: CGRect(origin: CGPoint(), size: size))
transition.updateTransformScale(node: controller, scale: scaledSize.width / size.width)
controller.updateLayout(size: size, transition: transition)
controller.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition)
controller.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition)
}
}
}

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

View File

@ -156,6 +156,7 @@ public final class WindowHostView {
public let eventView: UIView
public let isRotating: () -> Bool
public let systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError>
public let currentInterfaceOrientation: () -> UIInterfaceOrientation
let updateSupportedInterfaceOrientations: (UIInterfaceOrientationMask) -> Void
let updateDeferScreenEdgeGestures: (UIRectEdge) -> Void
@ -166,7 +167,7 @@ public final class WindowHostView {
var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)?
var presentNative: ((UIViewController) -> Void)?
var nativeController: (() -> UIViewController?)?
var updateSize: ((CGSize, Double) -> Void)?
var updateSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)?
var layoutSubviews: (() -> Void)?
var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)?
var isUpdatingOrientationLayout = false
@ -178,11 +179,12 @@ public final class WindowHostView {
var forEachController: (((ContainableController) -> Void) -> Void)?
var getAccessibilityElements: (() -> [Any]?)?
init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError>, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) {
init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError>, currentInterfaceOrientation: @escaping () -> UIInterfaceOrientation, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) {
self.containerView = containerView
self.eventView = eventView
self.isRotating = isRotating
self.systemUserInterfaceStyle = systemUserInterfaceStyle
self.currentInterfaceOrientation = currentInterfaceOrientation
self.updateSupportedInterfaceOrientations = updateSupportedInterfaceOrientations
self.updateDeferScreenEdgeGestures = updateDeferScreenEdgeGestures
self.updatePrefersOnScreenNavigationHidden = updatePrefersOnScreenNavigationHidden
@ -220,11 +222,11 @@ public extension UIView {
}
}
private func layoutMetricsForScreenSize(_ size: CGSize) -> LayoutMetrics {
private func layoutMetricsForScreenSize(size: CGSize, orientation: UIInterfaceOrientation?) -> LayoutMetrics {
if size.width > 690.0 && size.height > 650.0 {
return LayoutMetrics(widthClass: .regular, heightClass: .regular)
return LayoutMetrics(widthClass: .regular, heightClass: .regular, orientation: orientation)
} else {
return LayoutMetrics(widthClass: .compact, heightClass: .compact)
return LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: orientation)
}
}
@ -350,11 +352,13 @@ public class Window1 {
self.keyboardViewManager = nil
}
let isLandscape = boundsSize.width > boundsSize.height
let isLandscape = boundsSize.width > boundsSize.height
let safeInsets = self.deviceMetrics.safeInsets(inLandscape: isLandscape)
let onScreenNavigationHeight = self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(boundsSize), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning)
let orientation: UIInterfaceOrientation = self.hostView.currentInterfaceOrientation()
self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(size: boundsSize, orientation: orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning)
self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate)
self.presentationContext = PresentationContext()
self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost, parentView: self.hostView.containerView)
@ -406,8 +410,8 @@ public class Window1 {
self?.presentNative(controller)
}
self.hostView.updateSize = { [weak self] size, duration in
self?.updateSize(size, duration: duration)
self.hostView.updateSize = { [weak self] size, duration, orientation in
self?.updateSize(size, duration: duration, orientation: orientation)
}
self.hostView.layoutSubviews = { [weak self] in
@ -807,14 +811,14 @@ public class Window1 {
return self.viewController?.view.hitTest(point, with: event)
}
func updateSize(_ value: CGSize, duration: Double) {
func updateSize(_ value: CGSize, duration: Double, orientation: UIInterfaceOrientation) {
let transition: ContainedViewLayoutTransition
if !duration.isZero {
transition = .animated(duration: duration, curve: .easeInOut)
} else {
transition = .immediate
}
self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(value), safeInsets: self.deviceMetrics.safeInsets(inLandscape: value.width > value.height), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) }
self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(size: value, orientation: orientation), safeInsets: self.deviceMetrics.safeInsets(inLandscape: value.width > value.height), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) }
if let statusBarHost = self.statusBarHost, !statusBarHost.isApplicationInForeground {
self.layoutSubviews(force: true)
}
@ -1117,7 +1121,7 @@ public class Window1 {
}
let previousInputOffset = inputHeightOffsetForLayout(self.windowLayout)
self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(updatingLayout.layout.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight), upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver)
self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(size: updatingLayout.layout.size, orientation: updatingLayout.layout.metrics.orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight), upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver)
let childLayout = containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics)
let childLayoutUpdated = self.updatedContainerLayout != childLayout

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) {
let layout = ContainerViewLayout(
size: size,
metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact),
metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact, orientation: nil),
deviceMetrics: DeviceMetrics(screenSize: size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight, onScreenNavigationHeight: nil),
intrinsicInsets: intrinsicInsets,
safeInsets: safeInsets,

View File

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

View File

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

View File

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

View File

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

View File

@ -4,8 +4,12 @@ import Metal
#if os(iOS)
import Display
import UIKit
#else
import AppKit
import TGUIKit
#endif
import IOSurface
import ShelfPack
@ -662,13 +666,6 @@ public final class MetalEngine {
fileprivate var computeStates: [ObjectIdentifier: ComputeState] = [:]
init?(device: MTLDevice) {
let mainBundle = Bundle(for: Impl.self)
guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
self.device = device
@ -677,15 +674,42 @@ public final class MetalEngine {
}
self.commandQueue = commandQueue
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
self.library = library
let library: MTLLibrary?
guard let vertexFunction = library.makeFunction(name: "clearVertex") else {
#if os(iOS)
let mainBundle = Bundle(for: Impl.self)
guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else {
return nil
}
guard let fragmentFunction = library.makeFunction(name: "clearFragment") else {
guard let bundle = Bundle(path: path) else {
return nil
}
library = try? device.makeDefaultLibrary(bundle: bundle)
#else
let mainBundle = Bundle(for: Impl.self)
guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let path = bundle.path(forResource: "MetalEngineShaders", ofType: "metallib") else {
return nil
}
library = try? device.makeLibrary(URL: .init(fileURLWithPath: path))
#endif
guard let lib = library else {
return nil
}
self.library = lib
guard let vertexFunction = lib.makeFunction(name: "clearVertex") else {
return nil
}
guard let fragmentFunction = lib.makeFunction(name: "clearFragment") else {
return nil
}

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 let queue: Queue
private let monitor: NWPathMonitor
@ -133,7 +133,7 @@ private final class PathMonitor {
}
}
@available(iOSApplicationExtension 12.0, iOS 12.0, OSX 13.0, *)
@available(iOSApplicationExtension 12.0, iOS 12.0, macOS 14.0, *)
private final class SharedPathMonitor {
static let queue = Queue()
static let impl = QueueLocalObject<PathMonitor>(queue: queue, generate: {
@ -149,7 +149,7 @@ public enum Reachability {
}
public static var networkType: Signal<NetworkType, NoError> {
if #available(iOSApplicationExtension 12.0, iOS 12.0, OSX 13.0, *) {
if #available(iOSApplicationExtension 12.0, iOS 12.0, macOS 14.0, *) {
return Signal { subscriber in
let disposable = MetaDisposable()

View File

@ -73,6 +73,8 @@ public final class CallController: ViewController {
private let idleTimerExtensionDisposable = MetaDisposable()
public var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)?
public init(sharedContext: SharedAccountContext, account: Account, call: PresentationCall, easyDebugAccess: Bool) {
self.sharedContext = sharedContext
self.account = account
@ -136,7 +138,16 @@ public final class CallController: ViewController {
override public func loadDisplayNode() {
if self.sharedContext.immediateExperimentalUISettings.callUIV2 {
self.displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
let displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), easyDebugAccess: self.easyDebugAccess, call: self.call)
self.displayNode = displayNode
displayNode.restoreUIForPictureInPicture = { [weak self] completion in
guard let self, let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture else {
completion(false)
return
}
restoreUIForPictureInPicture(completion)
}
} else {
self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
}

View File

@ -17,6 +17,7 @@ import ImageBlur
import TelegramVoip
import MetalEngine
import DeviceAccess
import LibYuvBinding
final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol {
private let sharedContext: SharedAccountContext
@ -29,8 +30,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
private let callScreen: PrivateCallScreen
private var callScreenState: PrivateCallScreen.State?
private var shouldStayHiddenUntilConnection: Bool = false
private var callStartTimestamp: Double?
private var callState: PresentationCallState?
@ -47,6 +46,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
var callEnded: ((Bool) -> Void)?
var dismissedInteractively: (() -> Void)?
var dismissAllTooltips: (() -> Void)?
var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)?
private var emojiKey: (data: Data, resolvedKey: [String])?
private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)?
@ -67,7 +67,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
presentationData: PresentationData,
statusBar: StatusBar,
debugInfo: Signal<(String, String), NoError>,
shouldStayHiddenUntilConnection: Bool = false,
easyDebugAccess: Bool,
call: PresentationCall
) {
@ -80,8 +79,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
self.containerView = UIView()
self.callScreen = PrivateCallScreen()
self.shouldStayHiddenUntilConnection = shouldStayHiddenUntilConnection
super.init()
self.view.addSubview(self.containerView)
@ -122,6 +119,20 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
return
}
self.back?()
self.callScreen.beginPictureInPictureIfPossible()
}
self.callScreen.closeAction = { [weak self] in
guard let self else {
return
}
self.dismissedInteractively?()
}
self.callScreen.restoreUIForPictureInPicture = { [weak self] completion in
guard let self else {
completion(false)
return
}
self.restoreUIForPictureInPicture?(completion)
}
self.callScreenState = PrivateCallScreen.State(
@ -130,7 +141,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
shortName: " ",
avatarImage: nil,
audioOutput: .internalSpeaker,
isMicrophoneMuted: false,
isLocalAudioMuted: false,
isRemoteAudioMuted: false,
localVideo: nil,
remoteVideo: nil,
isRemoteBatteryLow: false
@ -145,8 +157,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
return
}
self.isMuted = isMuted
if callScreenState.isMicrophoneMuted != isMuted {
callScreenState.isMicrophoneMuted = isMuted
if callScreenState.isLocalAudioMuted != isMuted {
callScreenState.isLocalAudioMuted = isMuted
self.callScreenState = callScreenState
self.update(transition: .animated(duration: 0.3, curve: .spring))
}
@ -310,6 +322,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
case let .active(startTime, signalQuality, keyData):
self.callStartTimestamp = startTime
var signalQuality = signalQuality
signalQuality = 4
let _ = keyData
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
@ -320,7 +335,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
let _ = keyData
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 0.0),
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
emojiKey: self.resolvedEmojiKey(data: keyData)
))
case .terminating, .terminated:
@ -373,6 +388,13 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
callScreenState.isRemoteBatteryLow = false
}
switch callState.remoteAudioState {
case .muted:
callScreenState.isRemoteAudioMuted = true
case .active:
callScreenState.isRemoteAudioMuted = false
}
if self.callScreenState != callScreenState {
self.callScreenState = callScreenState
self.update(transition: .animated(duration: 0.35, curve: .spring))
@ -393,6 +415,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
return
}
callScreenState.name = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
callScreenState.shortName = peer.compactDisplayTitle
if self.currentPeer?.smallProfileImage != peer.smallProfileImage {
self.peerAvatarDisposable?.dispose()
@ -460,16 +483,14 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
self.containerView.layer.removeAnimation(forKey: "scale")
self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if !self.shouldStayHiddenUntilConnection {
self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3)
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3)
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
func animateOut(completion: @escaping () -> Void) {
self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
if !self.shouldStayHiddenUntilConnection || self.containerView.alpha > 0.0 {
if self.containerView.alpha > 0.0 {
self.containerView.layer.allowsGroupOpacity = true
self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
self?.containerView.layer.allowsGroupOpacity = false
@ -499,10 +520,18 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
transition.updateFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(view: self.callScreen, frame: CGRect(origin: CGPoint(), size: layout.size))
if let callScreenState = self.callScreenState {
if var callScreenState = self.callScreenState {
if case .terminated = callScreenState.lifecycleState {
callScreenState.isLocalAudioMuted = false
callScreenState.isRemoteAudioMuted = false
callScreenState.isRemoteBatteryLow = false
callScreenState.localVideo = nil
callScreenState.remoteVideo = nil
}
self.callScreen.update(
size: layout.size,
insets: layout.insets(options: [.statusBar]),
interfaceOrientation: layout.metrics.orientation ?? .portrait,
screenCornerRadius: layout.deviceMetrics.screenCornerRadius,
state: callScreenState,
transition: Transition(transition)
@ -511,7 +540,100 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
}
}
private func copyI420BufferToNV12Buffer(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer, pixelBuffer: CVPixelBuffer) -> Bool {
guard CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange else {
return false
}
guard CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) == buffer.width else {
return false
}
guard CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) == buffer.height else {
return false
}
let cvRet = CVPixelBufferLockBaseAddress(pixelBuffer, [])
if cvRet != kCVReturnSuccess {
return false
}
defer {
CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
}
guard let dstY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else {
return false
}
let dstStrideY = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
guard let dstUV = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else {
return false
}
let dstStrideUV = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
buffer.y.withUnsafeBytes { srcYBuffer in
guard let srcY = srcYBuffer.baseAddress else {
return
}
buffer.u.withUnsafeBytes { srcUBuffer in
guard let srcU = srcUBuffer.baseAddress else {
return
}
buffer.v.withUnsafeBytes { srcVBuffer in
guard let srcV = srcVBuffer.baseAddress else {
return
}
libyuv_I420ToNV12(
srcY.assumingMemoryBound(to: UInt8.self),
Int32(buffer.strideY),
srcU.assumingMemoryBound(to: UInt8.self),
Int32(buffer.strideU),
srcV.assumingMemoryBound(to: UInt8.self),
Int32(buffer.strideV),
dstY.assumingMemoryBound(to: UInt8.self),
Int32(dstStrideY),
dstUV.assumingMemoryBound(to: UInt8.self),
Int32(dstStrideUV),
Int32(buffer.width),
Int32(buffer.height)
)
}
}
}
return true
}
private final class AdaptedCallVideoSource: VideoSource {
final class I420DataBuffer: Output.DataBuffer {
private let buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer
override var pixelBuffer: CVPixelBuffer? {
let ioSurfaceProperties = NSMutableDictionary()
let options = NSMutableDictionary()
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
var pixelBuffer: CVPixelBuffer?
CVPixelBufferCreate(
kCFAllocatorDefault,
self.buffer.width,
self.buffer.height,
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
options,
&pixelBuffer
)
if let pixelBuffer, copyI420BufferToNV12Buffer(buffer: buffer, pixelBuffer: pixelBuffer) {
return pixelBuffer
} else {
return nil
}
}
init(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer) {
self.buffer = buffer
super.init()
}
}
private static let queue = Queue(name: "AdaptedCallVideoSource")
private var onUpdatedListeners = Bag<() -> Void>()
private(set) var currentOutput: Output?
@ -540,6 +662,8 @@ private final class AdaptedCallVideoSource: VideoSource {
rotationAngle = Float.pi * 3.0 / 2.0
}
let followsDeviceOrientation = videoFrameData.deviceRelativeOrientation != nil
var mirrorDirection: Output.MirrorDirection = []
var sourceId: Int = 0
@ -601,12 +725,45 @@ private final class AdaptedCallVideoSource: VideoSource {
output = Output(
resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)),
y: yTexture,
uv: uvTexture,
textureLayout: .biPlanar(Output.BiPlanarTextureLayout(
y: yTexture,
uv: uvTexture
)),
dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer),
rotationAngle: rotationAngle,
followsDeviceOrientation: followsDeviceOrientation,
mirrorDirection: mirrorDirection,
sourceId: sourceId
)
case let .i420(i420Buffer):
let width = i420Buffer.width
let height = i420Buffer.height
let _ = width
let _ = height
return
/*var cvMetalTextureY: CVMetalTexture?
var status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .r8Unorm, width, height, 0, &cvMetalTextureY)
guard status == kCVReturnSuccess, let yTexture = CVMetalTextureGetTexture(cvMetalTextureY!) else {
return
}
var cvMetalTextureUV: CVMetalTexture?
status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .rg8Unorm, width / 2, height / 2, 1, &cvMetalTextureUV)
guard status == kCVReturnSuccess, let uvTexture = CVMetalTextureGetTexture(cvMetalTextureUV!) else {
return
}
output = Output(
resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)),
y: yTexture,
uv: uvTexture,
dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer),
rotationAngle: rotationAngle,
followsDeviceOrientation: followsDeviceOrientation,
mirrorDirection: mirrorDirection,
sourceId: sourceId
)*/
default:
return
}

View File

@ -342,6 +342,32 @@ public final class PresentationCallImpl: PresentationCall {
}
private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?, audioSessionControl: ManagedAudioSessionControl?) {
self.reception = reception
if let ongoingContext = self.ongoingContext {
if self.receptionDisposable == nil, case .active = sessionState.state {
self.reception = 4
var canUpdate = false
self.receptionDisposable = (ongoingContext.reception
|> delay(1.0, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] reception in
if let strongSelf = self {
if let sessionState = strongSelf.sessionState {
if canUpdate {
strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf.reception = reception
}
} else {
strongSelf.reception = reception
}
}
})
canUpdate = true
}
}
if case .video = sessionState.type {
self.isVideo = true
}
@ -349,9 +375,10 @@ public final class PresentationCallImpl: PresentationCall {
let previousControl = self.audioSessionControl
self.sessionState = sessionState
self.callContextState = callContextState
self.reception = reception
self.audioSessionControl = audioSessionControl
let reception = self.reception
if previousControl != nil && audioSessionControl == nil {
print("updateSessionState \(sessionState.state) \(audioSessionControl != nil)")
}
@ -559,17 +586,6 @@ public final class PresentationCallImpl: PresentationCall {
}
})
self.receptionDisposable = (ongoingContext.reception
|> deliverOnMainQueue).start(next: { [weak self] reception in
if let strongSelf = self {
if let sessionState = strongSelf.sessionState {
strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf.reception = reception
}
}
})
self.audioLevelDisposable = (ongoingContext.audioLevel
|> deliverOnMainQueue).start(next: { [weak self] level in
if let strongSelf = self {

View File

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

View File

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

View File

@ -3,7 +3,11 @@ import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
import LinkPresentation
#if os(iOS)
import UIKit
#endif
import CoreServices
public enum WebpagePreviewResult: Equatable {
public struct Result: Equatable {
@ -14,9 +18,13 @@ public enum WebpagePreviewResult: Equatable {
case progress
case result(Result?)
}
#if os(macOS)
private typealias UIImage = NSImage
#endif
public func webpagePreview(account: Account, urls: [String], webpageId: MediaId? = nil) -> Signal<WebpagePreviewResult, NoError> {
return webpagePreviewWithProgress(account: account, urls: urls)
public func webpagePreview(account: Account, urls: [String], webpageId: MediaId? = nil, forPeerId: PeerId? = nil) -> Signal<WebpagePreviewResult, NoError> {
return webpagePreviewWithProgress(account: account, urls: urls, webpageId: webpageId, forPeerId: forPeerId)
|> mapToSignal { next -> Signal<WebpagePreviewResult, NoError> in
if case let .result(result) = next {
return .single(.result(result))
@ -35,7 +43,7 @@ public func normalizedWebpagePreviewUrl(url: String) -> String {
return url
}
public func webpagePreviewWithProgress(account: Account, urls: [String], webpageId: MediaId? = nil) -> Signal<WebpagePreviewWithProgressResult, NoError> {
public func webpagePreviewWithProgress(account: Account, urls: [String], webpageId: MediaId? = nil, forPeerId: PeerId? = nil) -> Signal<WebpagePreviewWithProgressResult, NoError> {
return account.postbox.transaction { transaction -> Signal<WebpagePreviewWithProgressResult, NoError> in
if let webpageId = webpageId, let webpage = transaction.getMedia(webpageId) as? TelegramMediaWebpage, let url = webpage.content.url {
var sourceUrl = url
@ -44,6 +52,108 @@ public func webpagePreviewWithProgress(account: Account, urls: [String], webpage
}
return .single(.result(WebpagePreviewResult.Result(webpage: webpage, sourceUrl: sourceUrl)))
} else {
if #available(iOS 13.0, macOS 10.15, *) {
if let forPeerId, forPeerId.namespace == Namespaces.Peer.SecretChat, let sourceUrl = urls.first, let url = URL(string: sourceUrl) {
let localHosts: [String] = [
"twitter.com",
"www.twitter.com",
"instagram.com",
"www.instagram.com",
"tiktok.com",
"www.tiktok.com"
]
if let host = url.host?.lowercased(), localHosts.contains(host) {
return Signal { subscriber in
subscriber.putNext(.progress(0.0))
let metadataProvider = LPMetadataProvider()
metadataProvider.shouldFetchSubresources = true
metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in
if let metadata = metadata {
let completeWithImage: (Data?) -> Void = { imageData in
var image: TelegramMediaImage?
if let imageData, let parsedImage = UIImage(data: imageData) {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
account.postbox.mediaBox.storeResourceData(resource.id, data: imageData)
image = TelegramMediaImage(
imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)),
representations: [
TelegramMediaImageRepresentation(
dimensions: PixelDimensions(width: Int32(parsedImage.size.width), height: Int32(parsedImage.size.height)),
resource: resource,
progressiveSizes: [],
immediateThumbnailData: nil,
hasVideo: false,
isPersonal: false
)
],
immediateThumbnailData: nil,
reference: nil,
partialReference: nil,
flags: []
)
}
var webpageType: String?
if image != nil {
webpageType = "photo"
}
let webpage = TelegramMediaWebpage(
webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)),
content: .Loaded(TelegramMediaWebpageLoadedContent(
url: sourceUrl,
displayUrl: metadata.url?.absoluteString ?? sourceUrl,
hash: 0,
type: webpageType,
websiteName: nil,
title: metadata.title,
text: metadata.value(forKey: "_summary") as? String,
embedUrl: nil,
embedType: nil,
embedSize: nil,
duration: nil,
author: nil,
isMediaLargeByDefault: true,
image: image,
file: nil,
story: nil,
attributes: [],
instantPage: nil
))
)
subscriber.putNext(.result(WebpagePreviewResult.Result(
webpage: webpage,
sourceUrl: sourceUrl
)))
subscriber.putCompletion()
}
if let imageProvider = metadata.imageProvider {
imageProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeImage as String, completionHandler: { imageUrl, _ in
guard let imageUrl, let imageData = try? Data(contentsOf: imageUrl) else {
completeWithImage(nil)
return
}
completeWithImage(imageData)
})
} else {
completeWithImage(nil)
}
} else {
subscriber.putNext(.result(nil))
subscriber.putCompletion()
}
})
return ActionDisposable {
metadataProvider.cancel()
}
}
}
}
}
return account.network.requestWithAdditionalInfo(Api.functions.messages.getWebPagePreview(flags: 0, message: urls.joined(separator: " "), entities: nil), info: .progress)
|> `catch` { _ -> Signal<NetworkRequestResult<Api.MessageMedia>, NoError> in
return .single(.result(.messageMediaEmpty))

View File

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

View File

@ -236,7 +236,7 @@ fragment half4 callBlobFragment(
return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha);
}
kernel void videoYUVToRGBA(
kernel void videoBiPlanarToRGBA(
texture2d<half, access::read> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureUV [[ texture(1) ]],
texture2d<half, access::write> outTexture [[ texture(2) ]],
@ -249,6 +249,22 @@ kernel void videoYUVToRGBA(
outTexture.write(color, threadPosition);
}
kernel void videoTriPlanarToRGBA(
texture2d<half, access::read> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureU [[ texture(1) ]],
texture2d<half, access::read> inTextureV [[ texture(2) ]],
texture2d<half, access::write> outTexture [[ texture(3) ]],
uint2 threadPosition [[ thread_position_in_grid ]]
) {
half y = inTextureY.read(threadPosition).r;
uint2 uvPosition = uint2(threadPosition.x / 2, threadPosition.y / 2);
half2 inUV = (inTextureU.read(uvPosition).r, inTextureV.read(uvPosition).r);
half2 uv = inUV - half2(0.5, 0.5);
half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0);
outTexture.write(color, threadPosition);
}
vertex QuadVertexOut mainVideoVertex(
const device Rectangle &rect [[ buffer(0) ]],
const device uint2 &mirror [[ buffer(1) ]],

View File

@ -58,8 +58,10 @@ final class ButtonGroupView: OverlayMaskContainerView {
private var buttons: [Button]?
private var buttonViews: [Button.Content.Key: ContentOverlayButton] = [:]
private var noticeViews: [AnyHashable: NoticeView] = [:]
private var closeButtonView: CloseButtonView?
var closePressed: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
@ -79,7 +81,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
return result
}
func update(size: CGSize, insets: UIEdgeInsets, controlsHidden: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat {
func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat {
self.buttons = buttons
let buttonSize: CGFloat = 56.0
@ -163,6 +165,46 @@ final class ButtonGroupView: OverlayMaskContainerView {
}
var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(buttons.count) - buttonSpacing * CGFloat(buttons.count - 1)) * 0.5)
if displayClose {
let closeButtonView: CloseButtonView
var closeButtonTransition = transition
var animateIn = false
if let current = self.closeButtonView {
closeButtonView = current
} else {
closeButtonTransition = closeButtonTransition.withAnimation(.none)
animateIn = true
closeButtonView = CloseButtonView()
self.closeButtonView = closeButtonView
self.addSubview(closeButtonView)
closeButtonView.pressAction = { [weak self] in
guard let self else {
return
}
self.closePressed?()
}
}
let closeButtonSize = CGSize(width: minWidth, height: buttonSize)
closeButtonView.update(text: "Close", size: closeButtonSize, transition: closeButtonTransition)
closeButtonTransition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((size.width - closeButtonSize.width) * 0.5), y: buttonY), size: closeButtonSize))
if animateIn && !transition.animation.isImmediate {
closeButtonView.animateIn()
}
} else {
if let closeButtonView = self.closeButtonView {
self.closeButtonView = nil
if !transition.animation.isImmediate {
closeButtonView.animateOut(completion: { [weak closeButtonView] in
closeButtonView?.removeFromSuperview()
})
} else {
closeButtonView.removeFromSuperview()
}
}
}
for button in buttons {
let title: String
let image: UIImage?
@ -213,9 +255,10 @@ final class ButtonGroupView: OverlayMaskContainerView {
Transition.immediate.setScale(view: buttonView, scale: 0.001)
buttonView.alpha = 0.0
transition.setScale(view: buttonView, scale: 1.0)
transition.setAlpha(view: buttonView, alpha: 1.0)
}
transition.setAlpha(view: buttonView, alpha: displayClose ? 0.0 : 1.0)
buttonTransition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize)))
buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: buttonTransition)
buttonX += buttonSize + buttonSpacing

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
if highlighted {
self.layer.removeAnimation(forKey: "opacity")
self.layer.removeAnimation(forKey: "transform")
self.layer.removeAnimation(forKey: "sublayerTransform")
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
transition.setScale(layer: self.layer, scale: topScale)
} else {

View File

@ -5,10 +5,10 @@ import ComponentFlow
final class EmojiExpandedInfoView: OverlayMaskContainerView {
private struct Params: Equatable {
var constrainedWidth: CGFloat
var width: CGFloat
init(constrainedWidth: CGFloat) {
self.constrainedWidth = constrainedWidth
init(width: CGFloat) {
self.width = width
}
}
@ -129,8 +129,8 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView {
return nil
}
func update(constrainedWidth: CGFloat, transition: Transition) -> CGSize {
let params = Params(constrainedWidth: constrainedWidth)
func update(width: CGFloat, transition: Transition) -> CGSize {
let params = Params(width: width)
if let currentLayout = self.currentLayout, currentLayout.params == params {
return currentLayout.size
}
@ -142,16 +142,12 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView {
private func update(params: Params, transition: Transition) -> CGSize {
let buttonHeight: CGFloat = 56.0
var constrainedWidth = params.constrainedWidth
constrainedWidth = min(constrainedWidth, 300.0)
let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition)
let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition)
let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition)
let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition)
let contentWidth: CGFloat = max(titleSize.width, textSize.width) + 26.0 * 2.0
let contentHeight = 78.0 + titleSize.height + 10.0 + textSize.height + 22.0 + buttonHeight
let size = CGSize(width: contentWidth, height: contentHeight)
let size = CGSize(width: params.width, height: contentHeight)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))

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

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 {
private let color: UIColor
private let smoothCorners: Bool
private var currentCornerRadius: CGFloat?
private var cornerImage: UIImage?
init(color: UIColor) {
init(color: UIColor, smoothCorners: Bool = false) {
self.color = color
self.smoothCorners = smoothCorners
super.init(image: nil)
if #available(iOS 13.0, *) {
self.layer.cornerCurve = .circular
}
}
required init?(coder: NSCoder) {
@ -26,10 +25,33 @@ final class RoundedCornersView: UIImageView {
guard let cornerRadius = self.currentCornerRadius else {
return
}
if let cornerImage = self.cornerImage, cornerImage.size.height == cornerRadius * 2.0 {
if cornerRadius == 0.0 {
if let cornerImage = self.cornerImage, cornerImage.size.width == 1.0 {
} else {
self.cornerImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in
context.setFillColor(self.color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5)
}
} else {
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color)
if self.smoothCorners {
let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
context.setFillColor(self.color.cgColor)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5)
}
} else {
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color)
}
}
}
self.image = self.cornerImage
self.clipsToBounds = false
@ -52,6 +74,14 @@ final class RoundedCornersView: UIImageView {
if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil {
self.layer.cornerRadius = previousCornerRadius
}
if #available(iOS 13.0, *) {
if self.smoothCorners {
self.layer.cornerCurve = .continuous
} else {
self.layer.cornerCurve = .circular
}
}
transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in
guard let self, completed else {
return

View File

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

View File

@ -16,18 +16,67 @@ public final class VideoSourceOutput {
public static let vertical = MirrorDirection(rawValue: 1 << 1)
}
open class DataBuffer {
open var pixelBuffer: CVPixelBuffer? {
return nil
}
public init() {
}
}
public final class BiPlanarTextureLayout {
public let y: MTLTexture
public let uv: MTLTexture
public init(y: MTLTexture, uv: MTLTexture) {
self.y = y
self.uv = uv
}
}
public final class TriPlanarTextureLayout {
public let y: MTLTexture
public let u: MTLTexture
public let v: MTLTexture
public init(y: MTLTexture, u: MTLTexture, v: MTLTexture) {
self.y = y
self.u = u
self.v = v
}
}
public enum TextureLayout {
case biPlanar(BiPlanarTextureLayout)
case triPlanar(TriPlanarTextureLayout)
}
public final class NativeDataBuffer: DataBuffer {
private let pixelBufferValue: CVPixelBuffer
override public var pixelBuffer: CVPixelBuffer? {
return self.pixelBufferValue
}
public init(pixelBuffer: CVPixelBuffer) {
self.pixelBufferValue = pixelBuffer
}
}
public let resolution: CGSize
public let y: MTLTexture
public let uv: MTLTexture
public let textureLayout: TextureLayout
public let dataBuffer: DataBuffer
public let rotationAngle: Float
public let followsDeviceOrientation: Bool
public let mirrorDirection: MirrorDirection
public let sourceId: Int
public init(resolution: CGSize, y: MTLTexture, uv: MTLTexture, rotationAngle: Float, mirrorDirection: MirrorDirection, sourceId: Int) {
public init(resolution: CGSize, textureLayout: TextureLayout, dataBuffer: DataBuffer, rotationAngle: Float, followsDeviceOrientation: Bool, mirrorDirection: MirrorDirection, sourceId: Int) {
self.resolution = resolution
self.y = y
self.uv = uv
self.textureLayout = textureLayout
self.dataBuffer = dataBuffer
self.rotationAngle = rotationAngle
self.followsDeviceOrientation = followsDeviceOrientation
self.mirrorDirection = mirrorDirection
self.sourceId = sourceId
}
@ -161,7 +210,18 @@ public final class FileVideoSource: VideoSource {
resolution.width = floor(resolution.width * self.sizeMultiplicator.x)
resolution.height = floor(resolution.height * self.sizeMultiplicator.y)
self.currentOutput = Output(resolution: resolution, y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, mirrorDirection: [], sourceId: self.sourceId)
self.currentOutput = Output(
resolution: resolution,
textureLayout: .biPlanar(Output.BiPlanarTextureLayout(
y: yTexture,
uv: uvTexture
)),
dataBuffer: Output.NativeDataBuffer(pixelBuffer: buffer),
rotationAngle: rotationAngle,
followsDeviceOrientation: false,
mirrorDirection: [],
sourceId: self.sourceId
)
return true
}
}

View File

@ -1,58 +1,14 @@
import Foundation
import AVFoundation
import AVKit
import UIKit
import Display
import MetalEngine
import ComponentFlow
import SwiftSignalKit
import UIKitRuntimeUtils
/*private final class EdgeTestLayer: MetalEngineSubjectLayer, MetalEngineSubject {
final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "edgeTestVertex"), let fragmentFunction = library.makeFunction(name: "edgeTestFragment") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
var internalData: MetalEngineSubjectInternalData?
func update(context: MetalEngineSubjectContext) {
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: 300, height: 300), edgeInset: 100), state: RenderState.self, layer: self, commands: { encoder, placement in
let effectiveRect = placement.effectiveRect
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width * 0.5), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
var color = SIMD4<Float>(1.0, 0.0, 0.0, 1.0)
encoder.setFragmentBytes(&color, length: 4 * 4, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
}*/
public final class PrivateCallScreen: OverlayMaskContainerView {
public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictureControllerDelegate {
public struct State: Equatable {
public struct SignalInfo: Equatable {
public var quality: Double
@ -100,7 +56,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
public var shortName: String
public var avatarImage: UIImage?
public var audioOutput: AudioOutput
public var isMicrophoneMuted: Bool
public var isLocalAudioMuted: Bool
public var isRemoteAudioMuted: Bool
public var localVideo: VideoSource?
public var remoteVideo: VideoSource?
public var isRemoteBatteryLow: Bool
@ -111,7 +68,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
shortName: String,
avatarImage: UIImage?,
audioOutput: AudioOutput,
isMicrophoneMuted: Bool,
isLocalAudioMuted: Bool,
isRemoteAudioMuted: Bool,
localVideo: VideoSource?,
remoteVideo: VideoSource?,
isRemoteBatteryLow: Bool
@ -121,7 +79,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.shortName = shortName
self.avatarImage = avatarImage
self.audioOutput = audioOutput
self.isMicrophoneMuted = isMicrophoneMuted
self.isLocalAudioMuted = isLocalAudioMuted
self.isRemoteAudioMuted = isRemoteAudioMuted
self.localVideo = localVideo
self.remoteVideo = remoteVideo
self.isRemoteBatteryLow = isRemoteBatteryLow
@ -143,7 +102,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
if lhs.audioOutput != rhs.audioOutput {
return false
}
if lhs.isMicrophoneMuted != rhs.isMicrophoneMuted {
if lhs.isLocalAudioMuted != rhs.isLocalAudioMuted {
return false
}
if lhs.isRemoteAudioMuted != rhs.isRemoteAudioMuted {
return false
}
if lhs.localVideo !== rhs.localVideo {
@ -162,12 +124,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
var interfaceOrientation: UIInterfaceOrientation
var screenCornerRadius: CGFloat
var state: State
init(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State) {
init(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State) {
self.size = size
self.insets = insets
self.interfaceOrientation = interfaceOrientation
self.screenCornerRadius = screenCornerRadius
self.state = state
}
@ -204,11 +168,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
private var activeLocalVideoSource: VideoSource?
private var waitingForFirstLocalVideoFrameDisposable: Disposable?
private var isUpdating: Bool = false
private var canAnimateAudioLevel: Bool = false
private var displayEmojiTooltip: Bool = false
private var isEmojiKeyExpanded: Bool = false
private var areControlsHidden: Bool = false
private var swapLocalAndRemoteVideo: Bool = false
private var isPictureInPictureActive: Bool = false
private var processedInitialAudioLevelBump: Bool = false
private var audioLevelBump: Float = 0.0
@ -224,6 +191,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
public var microhoneMuteAction: (() -> Void)?
public var endCallAction: (() -> Void)?
public var backAction: (() -> Void)?
public var closeAction: (() -> Void)?
public var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)?
private let pipView: PrivateCallPictureInPictureView
private var pipContentSource: AnyObject?
private var pipVideoCallViewController: UIViewController?
private var pipController: AVPictureInPictureController?
public override init(frame: CGRect) {
self.overlayContentsView = UIView()
@ -249,6 +223,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.backButtonView = BackButtonView(text: "Back")
self.pipView = PrivateCallPictureInPictureView(frame: CGRect(origin: CGPoint(), size: CGSize()))
super.init(frame: frame)
self.clipsToBounds = true
@ -264,10 +240,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.avatarTransformLayer.addSublayer(self.avatarLayer)
self.layer.addSublayer(self.avatarTransformLayer)
/*let edgeTestLayer = EdgeTestLayer()
edgeTestLayer.frame = CGRect(origin: CGPoint(x: 20.0, y: 100.0), size: CGSize(width: 100.0, height: 100.0))
self.layer.addSublayer(edgeTestLayer)*/
self.addSubview(self.videoContainerBackgroundView)
self.overlayContentsView.mask = self.maskContents
@ -310,6 +282,27 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
self.backAction?()
}
self.buttonGroupView.closePressed = { [weak self] in
guard let self else {
return
}
self.closeAction?()
}
if #available(iOS 16.0, *) {
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
pipVideoCallViewController.view.addSubview(self.pipView)
self.pipView.frame = pipVideoCallViewController.view.bounds
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.pipView.translatesAutoresizingMaskIntoConstraints = true
self.pipVideoCallViewController = pipVideoCallViewController
}
if let blurFilter = makeBlurFilter() {
blurFilter.setValue(10.0 as NSNumber, forKey: "inputRadius")
self.overlayContentsView.layer.filters = [blurFilter]
}
}
public required init?(coder: NSCoder) {
@ -335,6 +328,39 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
return result
}
public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.isPictureInPictureActive = true
if !self.isUpdating {
self.update(transition: .easeInOut(duration: 0.2))
}
}
public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.isPictureInPictureActive = false
if !self.isUpdating {
let wereControlsHidden = self.areControlsHidden
self.areControlsHidden = true
self.update(transition: .immediate)
if !wereControlsHidden {
self.areControlsHidden = false
self.update(transition: .spring(duration: 0.4))
}
}
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
if self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil {
if let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture {
restoreUIForPictureInPicture(completionHandler)
} else {
completionHandler(false)
}
} else {
completionHandler(false)
}
}
public func addIncomingAudioLevel(value: Float) {
if self.canAnimateAudioLevel {
self.targetAudioLevel = value
@ -385,8 +411,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
}
public func update(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State, transition: Transition) {
let params = Params(size: size, insets: insets, screenCornerRadius: screenCornerRadius, state: state)
public func beginPictureInPictureIfPossible() {
if let pipController = self.pipController, (self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil) {
pipController.startPictureInPicture()
}
}
public func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State, transition: Transition) {
let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: screenCornerRadius, state: state)
if self.params == params {
return
}
@ -487,6 +519,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
private func updateInternal(params: Params, transition: Transition) {
self.isUpdating = true
defer {
self.isUpdating = false
}
let genericAlphaTransition: Transition
switch transition.animation {
case .none:
@ -497,6 +534,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
let backgroundFrame = CGRect(origin: CGPoint(), size: params.size)
let wideContentWidth: CGFloat
if params.size.width < 500.0 {
wideContentWidth = params.size.width - 44.0 * 2.0
} else {
wideContentWidth = 400.0
}
var activeVideoSources: [(VideoContainerView.Key, VideoSource)] = []
if self.swapLocalAndRemoteVideo {
if let activeLocalVideoSource = self.activeLocalVideoSource {
@ -515,6 +559,41 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
let havePrimaryVideo = !activeVideoSources.isEmpty
if #available(iOS 16.0, *) {
if havePrimaryVideo, let pipVideoCallViewController = self.pipVideoCallViewController as? AVPictureInPictureVideoCallViewController {
if self.pipController == nil {
let pipContentSource = AVPictureInPictureController.ContentSource(activeVideoCallSourceView: self, contentViewController: pipVideoCallViewController)
let pipController = AVPictureInPictureController(contentSource: pipContentSource)
self.pipController = pipController
pipController.canStartPictureInPictureAutomaticallyFromInline = true
pipController.delegate = self
}
} else if let pipController = self.pipController {
self.pipController = nil
if pipController.isPictureInPictureActive {
pipController.stopPictureInPicture()
}
if self.isPictureInPictureActive {
self.isPictureInPictureActive = false
}
}
}
self.pipView.isRenderingEnabled = self.isPictureInPictureActive
self.pipView.video = self.activeRemoteVideoSource ?? self.activeLocalVideoSource
if let pipVideoCallViewController = self.pipVideoCallViewController {
if let video = self.pipView.video, let currentOutput = video.currentOutput {
var rotatedResolution = currentOutput.resolution
let resolvedRotationAngle = currentOutput.rotationAngle
if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 {
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
}
pipVideoCallViewController.preferredContentSize = rotatedResolution
}
}
let currentAreControlsHidden = havePrimaryVideo && self.areControlsHidden
let backgroundAspect: CGFloat = params.size.width / params.size.height
@ -554,7 +633,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
self.videoAction?()
}),
ButtonGroupView.Button(content: .microphone(isMuted: params.state.isMicrophoneMuted), action: { [weak self] in
ButtonGroupView.Button(content: .microphone(isMuted: params.state.isLocalAudioMuted), action: { [weak self] in
guard let self else {
return
}
@ -584,17 +663,24 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
var notices: [ButtonGroupView.Notice] = []
if params.state.isMicrophoneMuted {
if params.state.isLocalAudioMuted {
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "Your microphone is turned off"))
}
if params.state.isRemoteAudioMuted {
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "\(params.state.shortName)'s microphone is turned off"))
}
if params.state.remoteVideo != nil && params.state.localVideo == nil {
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "Your camera is turned off"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "Your camera is turned off"))
}
if params.state.isRemoteBatteryLow {
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "\(params.state.shortName)'s battery is low"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), text: "\(params.state.shortName)'s battery is low"))
}
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, controlsHidden: currentAreControlsHidden, buttons: buttons, notices: notices, transition: transition)
var displayClose = false
if case .terminated = params.state.lifecycleState {
displayClose = true
}
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, buttons: buttons, notices: notices, transition: transition)
var expandedEmojiKeyRect: CGRect?
if self.isEmojiKeyExpanded {
@ -632,7 +718,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
}
let emojiExpandedInfoSize = emojiExpandedInfoView.update(constrainedWidth: params.size.width - (params.insets.left + 16.0) * 2.0, transition: emojiExpandedInfoTransition)
let emojiExpandedInfoSize = emojiExpandedInfoView.update(width: wideContentWidth, transition: emojiExpandedInfoTransition)
let emojiExpandedInfoFrame = CGRect(origin: CGPoint(x: floor((params.size.width - emojiExpandedInfoSize.width) * 0.5), y: params.insets.top + 73.0), size: emojiExpandedInfoSize)
emojiExpandedInfoTransition.setPosition(view: emojiExpandedInfoView, position: CGPoint(x: emojiExpandedInfoFrame.minX + emojiExpandedInfoView.layer.anchorPoint.x * emojiExpandedInfoFrame.width, y: emojiExpandedInfoFrame.minY + emojiExpandedInfoView.layer.anchorPoint.y * emojiExpandedInfoFrame.height))
emojiExpandedInfoTransition.setBounds(view: emojiExpandedInfoView, bounds: CGRect(origin: CGPoint(), size: emojiExpandedInfoFrame.size))
@ -855,7 +941,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
videoContainerView.blurredContainerLayer.position = self.avatarTransformLayer.position
videoContainerView.blurredContainerLayer.bounds = self.avatarTransformLayer.bounds
videoContainerView.blurredContainerLayer.opacity = 0.0
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate)
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate)
Transition.immediate.setScale(view: videoContainerView, scale: self.currentAvatarAudioScale)
Transition.immediate.setScale(view: self.videoContainerBackgroundView, scale: self.currentAvatarAudioScale)
} else {
@ -865,7 +951,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
videoContainerView.blurredContainerLayer.position = expandedVideoFrame.center
videoContainerView.blurredContainerLayer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size)
videoContainerView.blurredContainerLayer.opacity = 0.0
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate)
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate)
}
}
@ -875,7 +961,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
videoContainerTransition.setPosition(layer: videoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
videoContainerTransition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
videoContainerTransition.setScale(layer: videoContainerView.blurredContainerLayer, scale: 1.0)
videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition)
videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition)
let alphaTransition: Transition
switch transition.animation {
@ -897,8 +983,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
}
alphaTransition.setAlpha(view: videoContainerView, alpha: 1.0)
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0)
let videoAlpha: CGFloat = self.isPictureInPictureActive ? 0.0 : 1.0
alphaTransition.setAlpha(view: videoContainerView, alpha: videoAlpha)
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: videoAlpha)
}
var removedVideoContainerIndices: [Int] = []
@ -910,7 +997,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
if self.videoContainerViews.count == 1 || (i == 0 && !havePrimaryVideo) {
let alphaTransition: Transition = genericAlphaTransition
videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition)
videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition)
transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center)
transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
@ -949,7 +1036,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
})
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
videoContainerView.update(size: params.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition)
videoContainerView.update(size: params.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition)
}
}
}

View File

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

View File

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

View File

@ -70,7 +70,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
public var timerUpdated: ((NSNumber?) -> Void)?
public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat {
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false)
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
}
public func caption() -> NSAttributedString {

View File

@ -97,6 +97,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
let contextSourceNode: ContextReferenceContentNode
private let textNode: ImmediateTextNode
private let iconNode: ASImageNode
private let backIconLayer: SimpleShapeLayer
private var animationNode: MoreIconNode?
private let backgroundNode: NavigationBackgroundNode
@ -117,6 +118,15 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.backIconLayer = SimpleShapeLayer()
self.backIconLayer.lineWidth = 3.0
self.backIconLayer.lineCap = .round
self.backIconLayer.lineJoin = .round
self.backIconLayer.strokeColor = UIColor.white.cgColor
self.backIconLayer.fillColor = nil
self.backIconLayer.isHidden = true
self.backIconLayer.path = try? convertSvgPath("M10.5,2 L1.5,11 L10.5,20 ")
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: true)
super.init(pointerStyle: .insetRectangle(-8.0, 2.0))
@ -128,6 +138,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
self.contextSourceNode.addSubnode(self.backgroundNode)
self.contextSourceNode.addSubnode(self.textNode)
self.contextSourceNode.addSubnode(self.iconNode)
self.contextSourceNode.layer.addSublayer(self.backIconLayer)
self.addSubnode(self.containerNode)
@ -146,13 +157,43 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
self.action?(self.contextSourceNode, nil)
}
func updateContentsColor(backgroundColor: UIColor, contentsColor: UIColor, transition: ContainedViewLayoutTransition) {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var boundingRect = self.bounds
if self.textNode.alpha != 0.0 {
boundingRect = boundingRect.union(self.textNode.frame)
}
boundingRect = boundingRect.insetBy(dx: -8.0, dy: -4.0)
if boundingRect.contains(point) {
return super.hitTest(self.bounds.center, with: event)
} else {
return nil
}
}
func updateContentsColor(backgroundColor: UIColor, contentsColor: UIColor, canBeExpanded: Bool, transition: ContainedViewLayoutTransition) {
self.contentsColor = contentsColor
self.backgroundNode.updateColor(color: backgroundColor, transition: transition)
transition.updateTintColor(layer: self.textNode.layer, color: self.contentsColor)
transition.updateTintColor(layer: self.iconNode.layer, color: self.contentsColor)
transition.updateStrokeColor(layer: self.backIconLayer, strokeColor: self.contentsColor)
switch self.key {
case .back:
transition.updateAlpha(layer: self.textNode.layer, alpha: canBeExpanded ? 1.0 : 0.0)
transition.updateTransformScale(node: self.textNode, scale: canBeExpanded ? 1.0 : 0.001)
var iconTransform = CATransform3DIdentity
iconTransform = CATransform3DScale(iconTransform, canBeExpanded ? 1.0 : 0.8, canBeExpanded ? 1.0 : 0.8, 1.0)
iconTransform = CATransform3DTranslate(iconTransform, canBeExpanded ? -7.0 : 0.0, 0.0, 0.0)
transition.updateTransform(node: self.iconNode, transform: CATransform3DGetAffineTransform(iconTransform))
transition.updateTransform(layer: self.backIconLayer, transform: CATransform3DGetAffineTransform(iconTransform))
transition.updateLineWidth(layer: self.backIconLayer, lineWidth: canBeExpanded ? 3.0 : 2.075)
default:
break
}
if let animationNode = self.animationNode {
transition.updateTintColor(layer: animationNode.imageNode.layer, color: self.contentsColor)
@ -184,9 +225,9 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
var animationState: MoreIconNodeState = .more
switch key {
case .back:
text = ""
text = presentationData.strings.Common_Back
accessibilityText = presentationData.strings.Common_Back
icon = NavigationBar.thinBackArrowImage
icon = NavigationBar.backArrowImage(color: .white)
case .edit:
text = presentationData.strings.Common_Edit
accessibilityText = text
@ -270,11 +311,19 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
}
let inset: CGFloat = 0.0
var textInset: CGFloat = 0.0
switch key {
case .back:
textInset += 11.0
default:
break
}
let resultSize: CGSize
let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize)
self.textNode.frame = textFrame
let textFrame = CGRect(origin: CGPoint(x: inset + textInset, y: floor((height - textSize.height) / 2.0)), size: textSize)
self.textNode.position = textFrame.center
self.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
if let animationNode = self.animationNode {
let animationSize = CGSize(width: 30.0, height: 30.0)
@ -286,7 +335,20 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size)
resultSize = size
} else if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size).offsetBy(dx: iconOffset.x, dy: iconOffset.y)
let iconFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size).offsetBy(dx: iconOffset.x, dy: iconOffset.y)
self.iconNode.position = iconFrame.center
self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
if case .back = key {
self.backIconLayer.position = iconFrame.center
self.backIconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
self.iconNode.isHidden = true
self.backIconLayer.isHidden = false
} else {
self.iconNode.isHidden = false
self.backIconLayer.isHidden = true
}
let size = CGSize(width: image.size.width + inset * 2.0, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)

View File

@ -36,18 +36,21 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode {
private var backgroundContentColor: UIColor = .clear
private var contentsColor: UIColor = .white
private var canBeExpanded: Bool = false
var performAction: ((PeerInfoHeaderNavigationButtonKey, ContextReferenceContentNode?, ContextGesture?) -> Void)?
func updateContentsColor(backgroundContentColor: UIColor, contentsColor: UIColor, transition: ContainedViewLayoutTransition) {
func updateContentsColor(backgroundContentColor: UIColor, contentsColor: UIColor, canBeExpanded: Bool, transition: ContainedViewLayoutTransition) {
self.backgroundContentColor = backgroundContentColor
self.contentsColor = contentsColor
for (_, button) in self.leftButtonNodes {
button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: transition)
button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: canBeExpanded, transition: transition)
transition.updateSublayerTransformOffset(layer: button.layer, offset: CGPoint(x: canBeExpanded ? -8.0 : 0.0, y: 0.0))
}
for (_, button) in self.rightButtonNodes {
button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: transition)
button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: canBeExpanded, transition: transition)
transition.updateSublayerTransformOffset(layer: button.layer, offset: CGPoint(x: canBeExpanded ? 8.0 : 0.0, y: 0.0))
}
}
@ -106,7 +109,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode {
buttonNode.frame = buttonFrame
buttonNode.alpha = 0.0
transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: .immediate)
buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: self.canBeExpanded, transition: .immediate)
} else {
transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
@ -202,7 +205,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode {
}
let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction)
if wasAdded {
buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: .immediate)
buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: self.canBeExpanded, transition: .immediate)
if key == .moreToSearch {
buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)

View File

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

View File

@ -844,7 +844,7 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
return
}
if let (updatedUrlPreviewState, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil), let updatedUrlPreviewState, let detectedUrl = updatedUrlPreviewState.detectedUrls.first {
if let (updatedUrlPreviewState, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil, forPeerId: selfController.chatLocation.peerId), let updatedUrlPreviewState, let detectedUrl = updatedUrlPreviewState.detectedUrls.first {
if let webpage = webpageCache[detectedUrl] {
progress?.set(.single(false))

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()
var inScope = true
var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)?
@ -301,7 +301,7 @@ func updateChatPresentationInterfaceStateImpl(
let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext
let editingUrlPreviewText: NSAttributedString? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText
if let (updatedEditingUrlPreviewState, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: selfController.context, currentQuery: selfController.editingUrlPreviewQueryState?.0) {
if let (updatedEditingUrlPreviewState, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: selfController.context, currentQuery: selfController.editingUrlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) {
selfController.editingUrlPreviewQueryState?.1.dispose()
var inScope = true
var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)?

View File

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

View File

@ -132,8 +132,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
private var groupCallDisposable: Disposable?
private var callController: CallController?
private var call: PresentationCall?
public let hasOngoingCall = ValuePromise<Bool>(false)
private let callState = Promise<PresentationCallState?>(nil)
private var awaitingCallConnectionDisposable: Disposable?
private var groupCallController: VoiceChatController?
public var currentGroupCallController: ViewController? {
@ -741,26 +743,49 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.callDisposable = (callManager.currentCallSignal
|> deliverOnMainQueue).start(next: { [weak self] call in
if let strongSelf = self {
if call !== strongSelf.callController?.call {
strongSelf.callController?.dismiss()
strongSelf.callController = nil
strongSelf.hasOngoingCall.set(false)
guard let self else {
return
}
if call !== self.call {
self.call = call
self.callController?.dismiss()
self.callController = nil
self.hasOngoingCall.set(false)
if let call {
self.callState.set(call.state
|> map(Optional.init))
self.hasOngoingCall.set(true)
setNotificationCall(call)
if let call = call {
mainWindow.hostView.containerView.endEditing(true)
let callController = CallController(sharedContext: strongSelf, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild)
strongSelf.callController = callController
strongSelf.mainWindow?.present(callController, on: .calls)
strongSelf.callState.set(call.state
|> map(Optional.init))
strongSelf.hasOngoingCall.set(true)
setNotificationCall(call)
} else {
strongSelf.callState.set(.single(nil))
strongSelf.hasOngoingCall.set(false)
setNotificationCall(nil)
if !call.isOutgoing && call.isIntegratedWithCallKit {
self.awaitingCallConnectionDisposable = (call.state
|> filter { state in
switch state.state {
case .ringing:
return false
default:
return true
}
}
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
self.presentControllerWithCurrentCall()
})
} else{
self.presentControllerWithCurrentCall()
}
} else {
self.callState.set(.single(nil))
self.hasOngoingCall.set(false)
self.awaitingCallConnectionDisposable?.dispose()
self.awaitingCallConnectionDisposable = nil
setNotificationCall(nil)
}
}
})
@ -951,6 +976,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.callDisposable?.dispose()
self.groupCallDisposable?.dispose()
self.callStateDisposable?.dispose()
self.awaitingCallConnectionDisposable?.dispose()
}
private var didPerformAccountSettingsImport = false
@ -1010,6 +1036,37 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
}
private func presentControllerWithCurrentCall() {
guard let call = self.call else {
return
}
if let currentCallController = self.callController {
if currentCallController.call === call {
self.navigateToCurrentCall()
return
} else {
self.callController = nil
currentCallController.dismiss()
}
}
self.mainWindow?.hostView.containerView.endEditing(true)
let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild)
self.callController = callController
callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in
guard let self, let callController else {
completion(false)
return
}
if callController.window == nil {
self.mainWindow?.present(callController, on: .calls)
}
completion(true)
}
self.mainWindow?.present(callController, on: .calls)
}
public func updateNotificationTokensRegistration() {
let sandbox: Bool
#if DEBUG

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

View File

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

View File

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