mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-02 12:48:45 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
465e8d96c8
@ -2426,6 +2426,8 @@ Unused sets are archived when you add more.";
|
||||
"Calls.RatingFeedback" = "Write a comment...";
|
||||
|
||||
"Call.StatusIncoming" = "Telegram Audio...";
|
||||
"Call.IncomingVoiceCall" = "Incoming Voice Call";
|
||||
"Call.IncomingVideoCall" = "Incoming Video Call";
|
||||
"Call.StatusRequesting" = "Contacting...";
|
||||
"Call.StatusWaiting" = "Waiting...";
|
||||
"Call.StatusRinging" = "Ringing...";
|
||||
|
||||
@ -67,6 +67,19 @@ public struct PresentationCallState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public final class PresentationCallVideoView {
|
||||
public let view: UIView
|
||||
public let setOnFirstFrameReceived: ((() -> Void)?) -> Void
|
||||
|
||||
public init(
|
||||
view: UIView,
|
||||
setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void
|
||||
) {
|
||||
self.view = view
|
||||
self.setOnFirstFrameReceived = setOnFirstFrameReceived
|
||||
}
|
||||
}
|
||||
|
||||
public protocol PresentationCall: class {
|
||||
var account: Account { get }
|
||||
var isIntegratedWithCallKit: Bool { get }
|
||||
@ -96,8 +109,8 @@ public protocol PresentationCall: class {
|
||||
func setCurrentAudioOutput(_ output: AudioSessionOutput)
|
||||
func debugInfo() -> Signal<(String, String), NoError>
|
||||
|
||||
func makeIncomingVideoView(completion: @escaping (UIView?) -> Void)
|
||||
func makeOutgoingVideoView(completion: @escaping (UIView?) -> Void)
|
||||
func makeIncomingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void)
|
||||
func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void)
|
||||
}
|
||||
|
||||
public protocol PresentationCallManager: class {
|
||||
|
||||
@ -34,6 +34,7 @@ protocol CallControllerNodeProtocol: class {
|
||||
|
||||
func animateIn()
|
||||
func animateOut(completion: @escaping () -> Void)
|
||||
func expandFromPipIfPossible()
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition)
|
||||
}
|
||||
@ -320,7 +321,11 @@ public final class CallController: ViewController {
|
||||
})
|
||||
}
|
||||
|
||||
@objc func backPressed() {
|
||||
@objc private func backPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
public func expandFromPipIfPossible() {
|
||||
self.controllerNode.expandFromPipIfPossible()
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +91,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
func update(size: CGSize, content: Content, text: String, transition: ContainedViewLayoutTransition) {
|
||||
let scaleFactor = size.width / self.largeButtonSize
|
||||
|
||||
let isSmall = self.largeButtonSize > size.width
|
||||
|
||||
self.effectView.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
self.contentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
self.overlayHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
@ -208,7 +210,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: labelFont, textColor: .white)
|
||||
}
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: 150.0, height: 100.0))
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: size.height + 5.0), size: textSize)
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: size.height + (isSmall ? 5.0 : 8.0)), size: textSize)
|
||||
if self.currentText.isEmpty {
|
||||
self.textNode.frame = textFrame
|
||||
if transition.isAnimated {
|
||||
|
||||
@ -78,7 +78,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
||||
|
||||
private var mode: CallControllerButtonsMode?
|
||||
|
||||
private var validLayout: CGFloat?
|
||||
private var validLayout: (CGFloat, CGFloat)?
|
||||
|
||||
var isMuted = false
|
||||
var isCameraPaused = false
|
||||
@ -94,27 +94,21 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func updateLayout(strings: PresentationStrings, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = constrainedWidth
|
||||
func updateLayout(strings: PresentationStrings, mode: CallControllerButtonsMode, constrainedWidth: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = (constrainedWidth, bottomInset)
|
||||
|
||||
self.mode = mode
|
||||
|
||||
if let mode = self.mode {
|
||||
self.updateButtonsLayout(strings: strings, mode: mode, width: constrainedWidth, animated: transition.isAnimated)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMode(strings: PresentationStrings, mode: CallControllerButtonsMode) {
|
||||
if self.mode != mode {
|
||||
let previousMode = self.mode
|
||||
self.mode = mode
|
||||
if let validLayout = self.validLayout {
|
||||
self.updateButtonsLayout(strings: strings, mode: mode, width: validLayout, animated: previousMode != nil)
|
||||
}
|
||||
return self.updateButtonsLayout(strings: strings, mode: mode, width: constrainedWidth, bottomInset: bottomInset, animated: transition.isAnimated)
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
private var appliedMode: CallControllerButtonsMode?
|
||||
|
||||
private func updateButtonsLayout(strings: PresentationStrings, mode: CallControllerButtonsMode, width: CGFloat, animated: Bool) {
|
||||
private func updateButtonsLayout(strings: PresentationStrings, mode: CallControllerButtonsMode, width: CGFloat, bottomInset: CGFloat, animated: Bool) -> CGFloat {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = .animated(duration: 0.3, curve: .spring)
|
||||
@ -151,6 +145,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
||||
let frame: CGRect
|
||||
}
|
||||
|
||||
let height: CGFloat
|
||||
|
||||
var buttons: [PlacedButton] = []
|
||||
switch mode {
|
||||
case .incoming(let speakerMode, let videoState), .outgoingRinging(let speakerMode, let videoState):
|
||||
@ -205,6 +201,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
||||
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: bottomButtonsLeftOffset, y: smallButtonSize + topBottomSpacing), size: CGSize(width: largeButtonSize, height: largeButtonSize))))
|
||||
bottomButtonsLeftOffset += largeButtonSize + bottomButtonsSpacing
|
||||
}
|
||||
|
||||
height = smallButtonSize + topBottomSpacing + largeButtonSize + max(bottomInset + 32.0, 46.0)
|
||||
case let .active(speakerMode, videoState):
|
||||
var topButtons: [ButtonDescription] = []
|
||||
|
||||
@ -238,9 +236,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
||||
let topButtonsWidth = CGFloat(topButtons.count) * smallButtonSize + CGFloat(topButtons.count - 1) * topButtonsSpacing
|
||||
var topButtonsLeftOffset = floor((width - topButtonsWidth) / 2.0)
|
||||
for button in topButtons {
|
||||
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: topButtonsLeftOffset, y: smallButtonSize + topBottomSpacing), size: CGSize(width: smallButtonSize, height: smallButtonSize))))
|
||||
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: topButtonsLeftOffset, y: 0.0), size: CGSize(width: smallButtonSize, height: smallButtonSize))))
|
||||
topButtonsLeftOffset += smallButtonSize + topButtonsSpacing
|
||||
}
|
||||
|
||||
height = smallButtonSize + max(bottomInset + 19.0, 46.0)
|
||||
}
|
||||
|
||||
let delayIncrement = 0.015
|
||||
@ -369,6 +369,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
||||
for key in removedKeys {
|
||||
self.buttonNodes.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
@objc func buttonPressed(_ button: CallControllerButtonItemNode) {
|
||||
|
||||
@ -14,21 +14,63 @@ import LocalizedPeerData
|
||||
import PhotoResources
|
||||
import CallsEmoji
|
||||
|
||||
private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
|
||||
return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t)))
|
||||
}
|
||||
|
||||
private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
|
||||
return (1.0 - value) * from + value * to
|
||||
}
|
||||
|
||||
private final class IncomingVideoNode: ASDisplayNode {
|
||||
private let videoView: UIView
|
||||
private let videoView: PresentationCallVideoView
|
||||
private var effectView: UIVisualEffectView?
|
||||
private var isBlurred: Bool = false
|
||||
|
||||
init(videoView: UIView) {
|
||||
private let isReadyUpdated: () -> Void
|
||||
private(set) var isReady: Bool = false
|
||||
private var isReadyTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(videoView: PresentationCallVideoView, isReadyUpdated: @escaping () -> Void) {
|
||||
self.videoView = videoView
|
||||
self.isReadyUpdated = isReadyUpdated
|
||||
|
||||
super.init()
|
||||
|
||||
self.view.addSubview(self.videoView)
|
||||
self.view.addSubview(self.videoView.view)
|
||||
|
||||
self.isReadyTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.isReady {
|
||||
strongSelf.isReady = true
|
||||
strongSelf.isReadyUpdated()
|
||||
}
|
||||
}, queue: .mainQueue())
|
||||
self.isReadyTimer?.start()
|
||||
|
||||
videoView.setOnFirstFrameReceived { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if !strongSelf.isReady {
|
||||
strongSelf.isReady = true
|
||||
strongSelf.isReadyTimer?.invalidate()
|
||||
strongSelf.isReadyUpdated()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.isReadyTimer?.invalidate()
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize) {
|
||||
self.videoView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.videoView.view.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
|
||||
func updateIsBlurred(isBlurred: Bool) {
|
||||
@ -41,7 +83,7 @@ private final class IncomingVideoNode: ASDisplayNode {
|
||||
if self.effectView == nil {
|
||||
let effectView = UIVisualEffectView()
|
||||
self.effectView = effectView
|
||||
effectView.frame = self.videoView.frame
|
||||
effectView.frame = self.videoView.view.frame
|
||||
self.view.addSubview(effectView)
|
||||
}
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
@ -57,26 +99,26 @@ private final class IncomingVideoNode: ASDisplayNode {
|
||||
|
||||
private final class OutgoingVideoNode: ASDisplayNode {
|
||||
private let videoTransformContainer: ASDisplayNode
|
||||
private let videoView: UIView
|
||||
private let videoView: PresentationCallVideoView
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
|
||||
private var effectView: UIVisualEffectView?
|
||||
private var isBlurred: Bool = false
|
||||
private var isExpanded: Bool = false
|
||||
private var currentCornerRadius: CGFloat = 0.0
|
||||
|
||||
var tapped: (() -> Void)?
|
||||
|
||||
init(videoView: UIView) {
|
||||
init(videoView: PresentationCallVideoView) {
|
||||
self.videoTransformContainer = ASDisplayNode()
|
||||
self.videoTransformContainer.clipsToBounds = true
|
||||
self.videoView = videoView
|
||||
self.videoView.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||||
self.videoView.view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.videoTransformContainer.view.addSubview(self.videoView)
|
||||
self.videoTransformContainer.view.addSubview(self.videoView.view)
|
||||
self.addSubnode(self.videoTransformContainer)
|
||||
//self.addSubnode(self.buttonNode)
|
||||
|
||||
@ -87,10 +129,10 @@ private final class OutgoingVideoNode: ASDisplayNode {
|
||||
self.tapped?()
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
||||
func updateLayout(size: CGSize, cornerRadius: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let videoFrame = CGRect(origin: CGPoint(), size: size)
|
||||
self.buttonNode.frame = videoFrame
|
||||
self.isExpanded = isExpanded
|
||||
self.currentCornerRadius = cornerRadius
|
||||
|
||||
let previousVideoFrame = self.videoTransformContainer.frame
|
||||
self.videoTransformContainer.frame = videoFrame
|
||||
@ -99,11 +141,11 @@ private final class OutgoingVideoNode: ASDisplayNode {
|
||||
transition.animateTransformScale(node: self.videoTransformContainer, from: previousVideoFrame.height / videoFrame.height)
|
||||
}
|
||||
|
||||
self.videoView.frame = videoFrame
|
||||
self.videoView.view.frame = videoFrame
|
||||
|
||||
transition.updateCornerRadius(layer: self.videoTransformContainer.layer, cornerRadius: isExpanded ? 0.0 : 16.0)
|
||||
transition.updateCornerRadius(layer: self.videoTransformContainer.layer, cornerRadius: self.currentCornerRadius)
|
||||
if let effectView = self.effectView {
|
||||
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: isExpanded ? 0.0 : 16.0)
|
||||
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: self.currentCornerRadius)
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,9 +159,9 @@ private final class OutgoingVideoNode: ASDisplayNode {
|
||||
if self.effectView == nil {
|
||||
let effectView = UIVisualEffectView()
|
||||
effectView.clipsToBounds = true
|
||||
effectView.layer.cornerRadius = self.isExpanded ? 0.0 : 16.0
|
||||
effectView.layer.cornerRadius = self.currentCornerRadius
|
||||
self.effectView = effectView
|
||||
effectView.frame = self.videoView.frame
|
||||
effectView.frame = self.videoView.view.frame
|
||||
self.view.addSubview(effectView)
|
||||
}
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
@ -133,7 +175,7 @@ private final class OutgoingVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
final class CallControllerNode: ViewControllerTracingNode, CallControllerNodeProtocol {
|
||||
private enum VideoNodeCorner {
|
||||
case topLeft
|
||||
case topRight
|
||||
@ -153,6 +195,7 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
private let easyDebugAccess: Bool
|
||||
private let call: PresentationCall
|
||||
|
||||
private let containerTransformationNode: ASDisplayNode
|
||||
private let containerNode: ASDisplayNode
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
@ -202,9 +245,21 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
var callEnded: ((Bool) -> Void)?
|
||||
var dismissedInteractively: (() -> Void)?
|
||||
|
||||
private var buttonsMode: CallControllerButtonsMode?
|
||||
|
||||
private var isUIHidden: Bool = false
|
||||
private var isVideoPaused: Bool = false
|
||||
|
||||
private enum PictureInPictureGestureState {
|
||||
case none
|
||||
case collapsing(didSelectCorner: Bool)
|
||||
case dragging(initialPosition: CGPoint, draggingPosition: CGPoint)
|
||||
}
|
||||
|
||||
private var pictureInPictureGestureState: PictureInPictureGestureState = .none
|
||||
private var pictureInPictureCorner: VideoNodeCorner = .topRight
|
||||
private var pictureInPictureTransitionFraction: CGFloat = 0.0
|
||||
|
||||
init(sharedContext: SharedAccountContext, account: Account, presentationData: PresentationData, statusBar: StatusBar, debugInfo: Signal<(String, String), NoError>, shouldStayHiddenUntilConnection: Bool = false, easyDebugAccess: Bool, call: PresentationCall) {
|
||||
self.sharedContext = sharedContext
|
||||
self.account = account
|
||||
@ -215,6 +270,9 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
self.easyDebugAccess = easyDebugAccess
|
||||
self.call = call
|
||||
|
||||
self.containerTransformationNode = ASDisplayNode()
|
||||
self.containerTransformationNode.clipsToBounds = true
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
if self.shouldStayHiddenUntilConnection {
|
||||
self.containerNode.alpha = 0.0
|
||||
@ -242,13 +300,10 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return UITracingLayerView()
|
||||
})
|
||||
|
||||
self.containerNode.backgroundColor = .black
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
self.addSubnode(self.containerTransformationNode)
|
||||
self.containerTransformationNode.addSubnode(self.containerNode)
|
||||
|
||||
self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: [])
|
||||
self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0)
|
||||
@ -377,7 +432,14 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
return
|
||||
}
|
||||
if let incomingVideoView = incomingVideoView {
|
||||
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView)
|
||||
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView, isReadyUpdated: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
})
|
||||
strongSelf.incomingVideoNode = incomingVideoNode
|
||||
strongSelf.containerNode.insertSubnode(incomingVideoNode, aboveSubnode: strongSelf.dimNode)
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
@ -399,8 +461,8 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
return
|
||||
}
|
||||
if let outgoingVideoView = outgoingVideoView {
|
||||
outgoingVideoView.backgroundColor = .black
|
||||
outgoingVideoView.clipsToBounds = true
|
||||
outgoingVideoView.view.backgroundColor = .black
|
||||
outgoingVideoView.view.clipsToBounds = true
|
||||
if let audioOutputState = strongSelf.audioOutputState, let currentOutput = audioOutputState.currentOutput {
|
||||
switch currentOutput {
|
||||
case .speaker, .builtin:
|
||||
@ -436,6 +498,7 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
}
|
||||
|
||||
if let incomingVideoNode = self.incomingVideoNode {
|
||||
incomingVideoNode.isHidden = !incomingVideoNode.isReady
|
||||
let isActive: Bool
|
||||
switch callState.remoteVideoState {
|
||||
case .inactive:
|
||||
@ -457,37 +520,42 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
|
||||
switch callState.state {
|
||||
case .waiting, .connecting:
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusConnecting, displayLogo: false)
|
||||
case let .requesting(ringing):
|
||||
if ringing {
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusRinging)
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusRinging, displayLogo: false)
|
||||
} else {
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusRequesting)
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusRequesting, displayLogo: false)
|
||||
}
|
||||
case .terminating:
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusEnded, displayLogo: false)
|
||||
case let .terminated(_, reason, _):
|
||||
if let reason = reason {
|
||||
switch reason {
|
||||
case let .ended(type):
|
||||
switch type {
|
||||
case .busy:
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusBusy)
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusBusy, displayLogo: false)
|
||||
case .hungUp, .missed:
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusEnded, displayLogo: false)
|
||||
}
|
||||
case .error:
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusFailed)
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusFailed, displayLogo: false)
|
||||
}
|
||||
} else {
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
||||
statusValue = .text(string: self.presentationData.strings.Call_StatusEnded, displayLogo: false)
|
||||
}
|
||||
case .ringing:
|
||||
var text = self.presentationData.strings.Call_StatusIncoming
|
||||
var text: String
|
||||
if self.call.isVideo {
|
||||
text = self.presentationData.strings.Call_IncomingVideoCall
|
||||
} else {
|
||||
text = self.presentationData.strings.Call_IncomingVoiceCall
|
||||
}
|
||||
if !self.statusNode.subtitle.isEmpty {
|
||||
text += "\n\(self.statusNode.subtitle)"
|
||||
}
|
||||
statusValue = .text(text)
|
||||
statusValue = .text(string: text, displayLogo: true)
|
||||
case .active(let timestamp, let reception, let keyVisualHash), .reconnecting(let timestamp, let reception, let keyVisualHash):
|
||||
let strings = self.presentationData.strings
|
||||
var isReconnecting = false
|
||||
@ -517,28 +585,6 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
}
|
||||
statusReception = reception
|
||||
}
|
||||
switch callState.state {
|
||||
case .terminated, .terminating:
|
||||
if !self.statusNode.alpha.isEqual(to: 0.5) {
|
||||
self.statusNode.alpha = 0.5
|
||||
self.buttonsNode.alpha = 0.5
|
||||
self.keyButtonNode.alpha = 0.5
|
||||
self.backButtonArrowNode.alpha = 0.5
|
||||
self.backButtonNode.alpha = 0.5
|
||||
|
||||
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
|
||||
self.buttonsNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
|
||||
self.keyButtonNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
|
||||
}
|
||||
default:
|
||||
if !self.statusNode.alpha.isEqual(to: 1.0) {
|
||||
self.statusNode.alpha = 1.0
|
||||
self.buttonsNode.alpha = 1.0
|
||||
self.keyButtonNode.alpha = 1.0
|
||||
self.backButtonArrowNode.alpha = 1.0
|
||||
self.backButtonNode.alpha = 1.0
|
||||
}
|
||||
}
|
||||
if self.shouldStayHiddenUntilConnection {
|
||||
switch callState.state {
|
||||
case .connecting, .active:
|
||||
@ -550,6 +596,15 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
self.statusNode.status = statusValue
|
||||
self.statusNode.reception = statusReception
|
||||
|
||||
if let callState = self.callState {
|
||||
switch callState.state {
|
||||
case .active, .connecting, .reconnecting:
|
||||
break
|
||||
default:
|
||||
self.isUIHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
self.updateButtonsMode()
|
||||
|
||||
if case let .terminated(id, _, reportRating) = callState.state, let callId = id {
|
||||
@ -598,24 +653,27 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
|
||||
switch callState.state {
|
||||
case .ringing:
|
||||
let buttonsMode: CallControllerButtonsMode = .incoming(speakerMode: mode, videoState: mappedVideoState)
|
||||
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsMode)
|
||||
self.buttonsMode = .incoming(speakerMode: mode, videoState: mappedVideoState)
|
||||
self.buttonsTerminationMode = buttonsMode
|
||||
case .waiting, .requesting:
|
||||
let buttonsMode: CallControllerButtonsMode = .outgoingRinging(speakerMode: mode, videoState: mappedVideoState)
|
||||
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsMode)
|
||||
self.buttonsMode = .outgoingRinging(speakerMode: mode, videoState: mappedVideoState)
|
||||
self.buttonsTerminationMode = buttonsMode
|
||||
case .active, .connecting, .reconnecting:
|
||||
let buttonsMode: CallControllerButtonsMode = .active(speakerMode: mode, videoState: mappedVideoState)
|
||||
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsMode)
|
||||
self.buttonsMode = .active(speakerMode: mode, videoState: mappedVideoState)
|
||||
self.buttonsTerminationMode = buttonsMode
|
||||
case .terminating, .terminated:
|
||||
if let buttonsTerminationMode = self.buttonsTerminationMode {
|
||||
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsTerminationMode)
|
||||
self.buttonsMode = buttonsTerminationMode
|
||||
} else {
|
||||
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: .active(speakerMode: mode, videoState: mappedVideoState))
|
||||
self.buttonsMode = .active(speakerMode: mode, videoState: mappedVideoState)
|
||||
}
|
||||
}
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.pictureInPictureTransitionFraction = 0.0
|
||||
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
@ -645,70 +703,110 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func expandFromPipIfPossible() {
|
||||
if self.pictureInPictureTransitionFraction.isEqual(to: 1.0), let (layout, navigationHeight) = self.validLayout {
|
||||
self.pictureInPictureTransitionFraction = 0.0
|
||||
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
private func calculatePreviewVideoRect(layout: ContainerViewLayout, navigationHeight: CGFloat) -> CGRect {
|
||||
let buttonsHeight: CGFloat = 190.0
|
||||
let buttonsOffset: CGFloat
|
||||
if layout.size.width.isEqual(to: 320.0) {
|
||||
if layout.size.height.isEqual(to: 480.0) {
|
||||
buttonsOffset = 60.0
|
||||
} else {
|
||||
buttonsOffset = 73.0
|
||||
}
|
||||
} else {
|
||||
buttonsOffset = 83.0
|
||||
}
|
||||
var uiDisplayTransition: CGFloat = self.isUIHidden ? 0.0 : 1.0
|
||||
uiDisplayTransition *= 1.0 - self.pictureInPictureTransitionFraction
|
||||
|
||||
let buttonsOriginY: CGFloat
|
||||
if self.isUIHidden {
|
||||
buttonsOriginY = layout.size.height + 40.0 - 80.0
|
||||
} else {
|
||||
buttonsOriginY = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom
|
||||
}
|
||||
let buttonsHeight: CGFloat = self.buttonsNode.bounds.height
|
||||
|
||||
let previewVideoSize = layout.size.aspectFitted(CGSize(width: 200.0, height: 200.0))
|
||||
var insets = layout.insets(options: .statusBar)
|
||||
insets.top += 44.0 + 8.0
|
||||
insets.bottom = buttonsHeight + 27.0
|
||||
insets.left = 20.0
|
||||
insets.right = 20.0
|
||||
|
||||
let expandedInset: CGFloat = 16.0
|
||||
|
||||
insets.top = interpolate(from: expandedInset, to: insets.top, value: uiDisplayTransition)
|
||||
insets.bottom = interpolate(from: expandedInset, to: insets.bottom, value: uiDisplayTransition)
|
||||
insets.left = interpolate(from: expandedInset, to: insets.left, value: 1.0 - self.pictureInPictureTransitionFraction)
|
||||
insets.right = interpolate(from: expandedInset, to: insets.right, value: 1.0 - self.pictureInPictureTransitionFraction)
|
||||
|
||||
let previewVideoSide = interpolate(from: 350.0, to: 200.0, value: 1.0 - self.pictureInPictureTransitionFraction)
|
||||
let previewVideoSize = layout.size.aspectFitted(CGSize(width: previewVideoSide, height: previewVideoSide))
|
||||
let previewVideoY: CGFloat
|
||||
let previewVideoX: CGFloat
|
||||
|
||||
switch self.outgoingVideoNodeCorner {
|
||||
case .topLeft:
|
||||
previewVideoX = 20.0
|
||||
if self.isUIHidden {
|
||||
previewVideoY = layout.insets(options: .statusBar).top + 8.0
|
||||
} else {
|
||||
previewVideoY = layout.insets(options: .statusBar).top + 44.0 + 8.0
|
||||
}
|
||||
previewVideoX = insets.left
|
||||
previewVideoY = insets.top
|
||||
case .topRight:
|
||||
previewVideoX = layout.size.width - previewVideoSize.width - 20.0
|
||||
if self.isUIHidden {
|
||||
previewVideoY = layout.insets(options: .statusBar).top + 8.0
|
||||
} else {
|
||||
previewVideoY = layout.insets(options: .statusBar).top + 44.0 + 8.0
|
||||
}
|
||||
previewVideoX = layout.size.width - previewVideoSize.width - insets.right
|
||||
previewVideoY = insets.top
|
||||
case .bottomLeft:
|
||||
previewVideoX = 20.0
|
||||
if self.isUIHidden {
|
||||
previewVideoY = layout.size.height - layout.intrinsicInsets.bottom - 8.0 - previewVideoSize.height
|
||||
} else {
|
||||
previewVideoY = buttonsOriginY + 100.0 - previewVideoSize.height
|
||||
}
|
||||
previewVideoX = insets.left
|
||||
previewVideoY = layout.size.height - insets.bottom - previewVideoSize.height
|
||||
case .bottomRight:
|
||||
previewVideoX = layout.size.width - previewVideoSize.width - 20.0
|
||||
if self.isUIHidden {
|
||||
previewVideoY = layout.size.height - layout.intrinsicInsets.bottom - 8.0 - previewVideoSize.height
|
||||
} else {
|
||||
previewVideoY = buttonsOriginY + 100.0 - previewVideoSize.height
|
||||
}
|
||||
previewVideoX = layout.size.width - previewVideoSize.width - insets.right
|
||||
previewVideoY = layout.size.height - insets.bottom - previewVideoSize.height
|
||||
}
|
||||
|
||||
return CGRect(origin: CGPoint(x: previewVideoX, y: previewVideoY), size: previewVideoSize)
|
||||
}
|
||||
|
||||
private func calculatePictureInPictureContainerRect(layout: ContainerViewLayout, navigationHeight: CGFloat) -> CGRect {
|
||||
let pictureInPictureTopInset: CGFloat = layout.insets(options: .statusBar).top + 44.0 + 8.0
|
||||
let pictureInPictureSideInset: CGFloat = 8.0
|
||||
let pictureInPictureSize = layout.size.fitted(CGSize(width: 240.0, height: 240.0))
|
||||
let pictureInPictureBottomInset: CGFloat = layout.insets(options: .input).bottom + 44.0 + 8.0
|
||||
|
||||
let containerPictureInPictureFrame: CGRect
|
||||
switch self.pictureInPictureCorner {
|
||||
case .topLeft:
|
||||
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: pictureInPictureSideInset, y: pictureInPictureTopInset), size: pictureInPictureSize)
|
||||
case .topRight:
|
||||
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: layout.size.width - pictureInPictureSideInset - pictureInPictureSize.width, y: pictureInPictureTopInset), size: pictureInPictureSize)
|
||||
case .bottomLeft:
|
||||
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: pictureInPictureSideInset, y: layout.size.height - pictureInPictureBottomInset - pictureInPictureSize.height), size: pictureInPictureSize)
|
||||
case .bottomRight:
|
||||
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: layout.size.width - pictureInPictureSideInset - pictureInPictureSize.width, y: layout.size.height - pictureInPictureBottomInset - pictureInPictureSize.height), size: pictureInPictureSize)
|
||||
}
|
||||
return containerPictureInPictureFrame
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
|
||||
let overlayAlpha: CGFloat = self.isUIHidden ? 0.0 : 1.0
|
||||
var uiDisplayTransition: CGFloat = self.isUIHidden ? 0.0 : 1.0
|
||||
uiDisplayTransition *= 1.0 - self.pictureInPictureTransitionFraction
|
||||
|
||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
let buttonsHeight: CGFloat
|
||||
if let buttonsMode = self.buttonsMode {
|
||||
buttonsHeight = self.buttonsNode.updateLayout(strings: self.presentationData.strings, mode: buttonsMode, constrainedWidth: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
|
||||
} else {
|
||||
buttonsHeight = 0.0
|
||||
}
|
||||
let defaultButtonsOriginY = layout.size.height - buttonsHeight
|
||||
let buttonsOriginY = interpolate(from: layout.size.height + 10.0, to: defaultButtonsOriginY, value: uiDisplayTransition)
|
||||
|
||||
var overlayAlpha: CGFloat = uiDisplayTransition
|
||||
|
||||
switch self.callState?.state {
|
||||
case .terminated, .terminating:
|
||||
overlayAlpha *= 0.5
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let containerFullScreenFrame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
let containerPictureInPictureFrame = self.calculatePictureInPictureContainerRect(layout: layout, navigationHeight: navigationBarHeight)
|
||||
|
||||
let containerFrame = interpolateFrame(from: containerFullScreenFrame, to: containerPictureInPictureFrame, t: self.pictureInPictureTransitionFraction)
|
||||
|
||||
transition.updateFrame(node: self.containerTransformationNode, frame: containerFrame)
|
||||
transition.updateSublayerTransformScale(node: self.containerTransformationNode, scale: min(1.0, containerFrame.width / layout.size.width * 1.01))
|
||||
transition.updateCornerRadius(layer: self.containerTransformationNode.layer, cornerRadius: self.pictureInPictureTransitionFraction * 10.0)
|
||||
|
||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: (containerFrame.width - layout.size.width) / 2.0, y: floor(containerFrame.height - layout.size.height) / 2.0), size: layout.size))
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
if let keyPreviewNode = self.keyPreviewNode {
|
||||
@ -751,18 +849,6 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
|
||||
statusOffset += layout.safeInsets.top
|
||||
|
||||
let buttonsHeight: CGFloat = 190.0
|
||||
let buttonsOffset: CGFloat
|
||||
if layout.size.width.isEqual(to: 320.0) {
|
||||
if layout.size.height.isEqual(to: 480.0) {
|
||||
buttonsOffset = 60.0
|
||||
} else {
|
||||
buttonsOffset = 73.0
|
||||
}
|
||||
} else {
|
||||
buttonsOffset = 83.0
|
||||
}
|
||||
|
||||
let statusHeight = self.statusNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
|
||||
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusOffset), size: CGSize(width: layout.size.width, height: statusHeight)))
|
||||
transition.updateAlpha(node: self.statusNode, alpha: overlayAlpha)
|
||||
@ -770,13 +856,6 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
let videoPausedSize = self.videoPausedNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: 100.0))
|
||||
transition.updateFrame(node: self.videoPausedNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - videoPausedSize.width) / 2.0), y: floor((layout.size.height - videoPausedSize.height) / 2.0)), size: videoPausedSize))
|
||||
|
||||
self.buttonsNode.updateLayout(strings: self.presentationData.strings, constrainedWidth: layout.size.width, transition: transition)
|
||||
let buttonsOriginY: CGFloat
|
||||
if self.isUIHidden {
|
||||
buttonsOriginY = layout.size.height + 40.0 - 80.0
|
||||
} else {
|
||||
buttonsOriginY = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom
|
||||
}
|
||||
transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonsOriginY), size: CGSize(width: layout.size.width, height: buttonsHeight)))
|
||||
transition.updateAlpha(node: self.buttonsNode, alpha: overlayAlpha)
|
||||
|
||||
@ -801,18 +880,18 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
if outgoingVideoNode.frame.isEmpty {
|
||||
outgoingVideoTransition = .immediate
|
||||
}
|
||||
if self.incomingVideoNode == nil {
|
||||
outgoingVideoNode.frame = fullscreenVideoFrame
|
||||
outgoingVideoNode.updateLayout(size: layout.size, isExpanded: true, transition: outgoingVideoTransition)
|
||||
} else {
|
||||
if let incomingVideoNode = self.incomingVideoNode, incomingVideoNode.isReady {
|
||||
if self.minimizedVideoDraggingPosition == nil {
|
||||
if self.outgoingVideoExplicitelyFullscreen {
|
||||
outgoingVideoTransition.updateFrame(node: outgoingVideoNode, frame: fullscreenVideoFrame)
|
||||
} else {
|
||||
outgoingVideoTransition.updateFrame(node: outgoingVideoNode, frame: previewVideoFrame)
|
||||
}
|
||||
outgoingVideoNode.updateLayout(size: outgoingVideoNode.frame.size, isExpanded: self.outgoingVideoExplicitelyFullscreen, transition: outgoingVideoTransition)
|
||||
outgoingVideoNode.updateLayout(size: outgoingVideoNode.frame.size, cornerRadius: interpolate(from: self.outgoingVideoExplicitelyFullscreen ? 0.0 : 14.0, to: 24.0, value: self.pictureInPictureTransitionFraction), transition: outgoingVideoTransition)
|
||||
}
|
||||
} else {
|
||||
outgoingVideoNode.frame = fullscreenVideoFrame
|
||||
outgoingVideoNode.updateLayout(size: layout.size, cornerRadius: 0.0, transition: outgoingVideoTransition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -861,12 +940,27 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
if let _ = self.keyPreviewNode {
|
||||
if !self.pictureInPictureTransitionFraction.isZero {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.pictureInPictureTransitionFraction = 0.0
|
||||
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
} else if let _ = self.keyPreviewNode {
|
||||
self.backPressed()
|
||||
} else {
|
||||
if self.incomingVideoNode != nil || self.outgoingVideoNode != nil {
|
||||
self.isUIHidden = !self.isUIHidden
|
||||
if let (layout, navigationBarHeight) = self.validLayout {
|
||||
var updated = false
|
||||
if let callState = self.callState {
|
||||
switch callState.state {
|
||||
case .active, .connecting, .reconnecting:
|
||||
self.isUIHidden = !self.isUIHidden
|
||||
updated = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if updated, let (layout, navigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
} else {
|
||||
@ -1034,13 +1128,15 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
let location = recognizer.location(in: self.view)
|
||||
//let translation = recognizer.translation(in: self.view)
|
||||
//location.x += translation.x
|
||||
//location.y += translation.y
|
||||
if let _ = self.incomingVideoNode, let outgoingVideoNode = self.outgoingVideoNode, outgoingVideoNode.frame.contains(location) {
|
||||
if self.self.pictureInPictureTransitionFraction.isZero, let _ = self.incomingVideoNode, let outgoingVideoNode = self.outgoingVideoNode, outgoingVideoNode.frame.contains(location) {
|
||||
self.minimizedVideoInitialPosition = outgoingVideoNode.position
|
||||
} else {
|
||||
self.minimizedVideoInitialPosition = nil
|
||||
if !self.pictureInPictureTransitionFraction.isZero {
|
||||
self.pictureInPictureGestureState = .dragging(initialPosition: self.containerTransformationNode.position, draggingPosition: self.containerTransformationNode.position)
|
||||
} else {
|
||||
self.pictureInPictureGestureState = .collapsing(didSelectCorner: false)
|
||||
}
|
||||
}
|
||||
case .changed:
|
||||
if let outgoingVideoNode = self.outgoingVideoNode, let minimizedVideoInitialPosition = self.minimizedVideoInitialPosition {
|
||||
@ -1049,10 +1145,43 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
self.minimizedVideoDraggingPosition = minimizedVideoDraggingPosition
|
||||
outgoingVideoNode.position = minimizedVideoDraggingPosition
|
||||
} else {
|
||||
let offset = recognizer.translation(in: self.view).y
|
||||
var bounds = self.bounds
|
||||
bounds.origin.y = -offset
|
||||
self.bounds = bounds
|
||||
switch self.pictureInPictureGestureState {
|
||||
case .none:
|
||||
let offset = recognizer.translation(in: self.view).y
|
||||
var bounds = self.bounds
|
||||
bounds.origin.y = -offset
|
||||
self.bounds = bounds
|
||||
case let .collapsing(didSelectCorner):
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
let offset = recognizer.translation(in: self.view)
|
||||
if !didSelectCorner {
|
||||
self.pictureInPictureGestureState = .collapsing(didSelectCorner: true)
|
||||
if offset.x < 0.0 {
|
||||
self.pictureInPictureCorner = .topLeft
|
||||
} else {
|
||||
self.pictureInPictureCorner = .topRight
|
||||
}
|
||||
}
|
||||
let maxOffset: CGFloat = min(300.0, layout.size.height / 2.0)
|
||||
|
||||
let offsetTransition = max(0.0, min(1.0, abs(offset.y) / maxOffset))
|
||||
self.pictureInPictureTransitionFraction = offsetTransition
|
||||
switch self.pictureInPictureCorner {
|
||||
case .topRight, .bottomRight:
|
||||
self.pictureInPictureCorner = offset.y < 0.0 ? .topRight : .bottomRight
|
||||
case .topLeft, .bottomLeft:
|
||||
self.pictureInPictureCorner = offset.y < 0.0 ? .topLeft : .bottomLeft
|
||||
}
|
||||
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
|
||||
}
|
||||
case .dragging(let initialPosition, var draggingPosition):
|
||||
let translation = recognizer.translation(in: self.view)
|
||||
draggingPosition.x = initialPosition.x + translation.x
|
||||
draggingPosition.y = initialPosition.y + translation.y
|
||||
self.pictureInPictureGestureState = .dragging(initialPosition: initialPosition, draggingPosition: draggingPosition)
|
||||
self.containerTransformationNode.position = draggingPosition
|
||||
}
|
||||
}
|
||||
case .cancelled, .ended:
|
||||
if let outgoingVideoNode = self.outgoingVideoNode, let _ = self.minimizedVideoInitialPosition, let minimizedVideoDraggingPosition = self.minimizedVideoDraggingPosition {
|
||||
@ -1067,25 +1196,62 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||
outgoingVideoNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: minimizedVideoDraggingPosition.x - videoFrame.midX, y: minimizedVideoDraggingPosition.y - videoFrame.midY)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: 0.0, initialVelocity: 0.0, damping: 110.0, removeOnCompletion: true, additive: true, completion: nil)
|
||||
}
|
||||
} else {
|
||||
let velocity = recognizer.velocity(in: self.view).y
|
||||
if abs(velocity) < 100.0 {
|
||||
var bounds = self.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint()
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
} else {
|
||||
var bounds = self.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
|
||||
self?.dismissedInteractively?()
|
||||
})
|
||||
switch self.pictureInPictureGestureState {
|
||||
case .none:
|
||||
let velocity = recognizer.velocity(in: self.view).y
|
||||
if abs(velocity) < 100.0 {
|
||||
var bounds = self.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint()
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
} else {
|
||||
var bounds = self.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
|
||||
self?.dismissedInteractively?()
|
||||
})
|
||||
}
|
||||
case .collapsing:
|
||||
self.pictureInPictureGestureState = .none
|
||||
let velocity = recognizer.velocity(in: self.view).y
|
||||
if abs(velocity) < 100.0 {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.pictureInPictureTransitionFraction = 0.0
|
||||
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
} else {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.pictureInPictureTransitionFraction = 1.0
|
||||
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
case let .dragging(initialPosition, _):
|
||||
self.pictureInPictureGestureState = .none
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
let translation = recognizer.translation(in: self.view)
|
||||
let draggingPosition = CGPoint(x: initialPosition.x + translation.x, y: initialPosition.y + translation.y)
|
||||
self.pictureInPictureCorner = self.nodeLocationForPosition(layout: layout, position: draggingPosition, velocity: recognizer.velocity(in: self.view))
|
||||
|
||||
let containerFrame = self.calculatePictureInPictureContainerRect(layout: layout, navigationHeight: navigationHeight)
|
||||
self.containerTransformationNode.frame = containerFrame
|
||||
containerTransformationNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: draggingPosition.x - containerFrame.midX, y: draggingPosition.y - containerFrame.midY)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: 0.0, initialVelocity: 0.0, damping: 110.0, removeOnCompletion: true, additive: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.containerTransformationNode.frame.contains(point) {
|
||||
return self.containerTransformationNode.view.hitTest(self.view.convert(point, to: self.containerTransformationNode.view), with: event)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,13 +11,13 @@ private let compactStatusFont = Font.regular(18.0)
|
||||
private let regularStatusFont = Font.regular(18.0)
|
||||
|
||||
enum CallControllerStatusValue: Equatable {
|
||||
case text(String)
|
||||
case text(string: String, displayLogo: Bool)
|
||||
case timer((String) -> String, Double)
|
||||
|
||||
static func ==(lhs: CallControllerStatusValue, rhs: CallControllerStatusValue) -> Bool {
|
||||
switch lhs {
|
||||
case let .text(text):
|
||||
if case .text(text) = rhs {
|
||||
case let .text(text, displayLogo):
|
||||
if case .text(text, displayLogo) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -37,10 +37,11 @@ final class CallControllerStatusNode: ASDisplayNode {
|
||||
private let statusNode: TextNode
|
||||
private let statusMeasureNode: TextNode
|
||||
private let receptionNode: CallControllerReceptionNode
|
||||
private let logoNode: ASImageNode
|
||||
|
||||
var title: String = ""
|
||||
var subtitle: String = ""
|
||||
var status: CallControllerStatusValue = .text("") {
|
||||
var status: CallControllerStatusValue = .text(string: "", displayLogo: false) {
|
||||
didSet {
|
||||
if self.status != oldValue {
|
||||
self.statusTimer?.invalidate()
|
||||
@ -96,6 +97,10 @@ final class CallControllerStatusNode: ASDisplayNode {
|
||||
self.receptionNode = CallControllerReceptionNode()
|
||||
self.receptionNode.alpha = 0.0
|
||||
|
||||
self.logoNode = ASImageNode()
|
||||
self.logoNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallTitleLogo"), color: .white)
|
||||
self.logoNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
@ -103,6 +108,7 @@ final class CallControllerStatusNode: ASDisplayNode {
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.statusNode)
|
||||
self.addSubnode(self.receptionNode)
|
||||
self.addSubnode(self.logoNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -125,29 +131,34 @@ final class CallControllerStatusNode: ASDisplayNode {
|
||||
var statusOffset: CGFloat = 0.0
|
||||
let statusText: String
|
||||
let statusMeasureText: String
|
||||
var statusDisplayLogo: Bool = false
|
||||
switch self.status {
|
||||
case let .text(text):
|
||||
statusText = text
|
||||
statusMeasureText = text
|
||||
case let .timer(format, referenceTime):
|
||||
let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime)
|
||||
let durationString: String
|
||||
let measureDurationString: String
|
||||
if duration > 60 * 60 {
|
||||
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
|
||||
measureDurationString = "00:00:00"
|
||||
} else {
|
||||
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
|
||||
measureDurationString = "00:00"
|
||||
}
|
||||
statusText = format(durationString)
|
||||
statusMeasureText = format(measureDurationString)
|
||||
if self.reception != nil {
|
||||
statusOffset += 8.0
|
||||
}
|
||||
case let .text(text, displayLogo):
|
||||
statusText = text
|
||||
statusMeasureText = text
|
||||
statusDisplayLogo = displayLogo
|
||||
if displayLogo {
|
||||
statusOffset += 10.0
|
||||
}
|
||||
case let .timer(format, referenceTime):
|
||||
let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime)
|
||||
let durationString: String
|
||||
let measureDurationString: String
|
||||
if duration > 60 * 60 {
|
||||
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
|
||||
measureDurationString = "00:00:00"
|
||||
} else {
|
||||
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
|
||||
measureDurationString = "00:00"
|
||||
}
|
||||
statusText = format(durationString)
|
||||
statusMeasureText = format(measureDurationString)
|
||||
if self.reception != nil {
|
||||
statusOffset += 8.0
|
||||
}
|
||||
}
|
||||
|
||||
let spacing: CGFloat = 4.0
|
||||
let spacing: CGFloat = 1.0
|
||||
let (titleLayout, titleApply) = TextNode.asyncLayout(self.titleNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: nameFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
|
||||
let (statusMeasureLayout, statusMeasureApply) = TextNode.asyncLayout(self.statusMeasureNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusMeasureText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
|
||||
let (statusLayout, statusApply) = TextNode.asyncLayout(self.statusNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
|
||||
@ -159,6 +170,10 @@ final class CallControllerStatusNode: ASDisplayNode {
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - titleLayout.size.width) / 2.0), y: 0.0), size: titleLayout.size)
|
||||
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0) + statusOffset, y: titleLayout.size.height + spacing), size: statusLayout.size)
|
||||
self.receptionNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX - receptionNodeSize.width, y: titleLayout.size.height + spacing + 9.0), size: receptionNodeSize)
|
||||
self.logoNode.isHidden = !statusDisplayLogo
|
||||
if let image = self.logoNode.image {
|
||||
self.logoNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX - image.size.width - 7.0, y: self.statusNode.frame.minY + 5.0), size: image.size)
|
||||
}
|
||||
|
||||
return titleLayout.size.height + spacing + statusLayout.size.height
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
|
||||
private var outgoingVideoViewRequested: Bool = false
|
||||
private let backButtonArrowNode: ASImageNode
|
||||
private let backButtonNode: HighlightableButtonNode
|
||||
private let statusNode: CallControllerStatusNode
|
||||
private let statusNode: LegacyCallControllerStatusNode
|
||||
private let videoPausedNode: ImmediateTextNode
|
||||
private let buttonsNode: LegacyCallControllerButtonsNode
|
||||
private var keyPreviewNode: CallControllerKeyPreviewNode?
|
||||
@ -168,7 +168,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
|
||||
self.backButtonArrowNode.image = NavigationBarTheme.generateBackArrowImage(color: .white)
|
||||
self.backButtonNode = HighlightableButtonNode()
|
||||
|
||||
self.statusNode = CallControllerStatusNode()
|
||||
self.statusNode = LegacyCallControllerStatusNode()
|
||||
|
||||
self.videoPausedNode = ImmediateTextNode()
|
||||
self.videoPausedNode.alpha = 0.0
|
||||
@ -291,7 +291,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
|
||||
func updateCallState(_ callState: PresentationCallState) {
|
||||
self.callState = callState
|
||||
|
||||
let statusValue: CallControllerStatusValue
|
||||
let statusValue: LegacyCallControllerStatusValue
|
||||
var statusReception: Int32?
|
||||
|
||||
switch callState.videoState {
|
||||
@ -304,7 +304,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
|
||||
}
|
||||
if let incomingVideoView = incomingVideoView {
|
||||
strongSelf.setCurrentAudioOutput?(.speaker)
|
||||
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView)
|
||||
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView.view)
|
||||
strongSelf.incomingVideoNode = incomingVideoNode
|
||||
strongSelf.containerNode.insertSubnode(incomingVideoNode, aboveSubnode: strongSelf.dimNode)
|
||||
strongSelf.statusNode.isHidden = true
|
||||
@ -320,7 +320,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let outgoingVideoView = outgoingVideoView {
|
||||
if let outgoingVideoView = outgoingVideoView?.view {
|
||||
outgoingVideoView.backgroundColor = .black
|
||||
outgoingVideoView.clipsToBounds = true
|
||||
strongSelf.setCurrentAudioOutput?(.speaker)
|
||||
@ -349,7 +349,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let outgoingVideoView = outgoingVideoView {
|
||||
if let outgoingVideoView = outgoingVideoView?.view {
|
||||
outgoingVideoView.backgroundColor = .black
|
||||
outgoingVideoView.clipsToBounds = true
|
||||
outgoingVideoView.layer.cornerRadius = 16.0
|
||||
@ -569,6 +569,9 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func expandFromPipIfPossible() {
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
|
||||
|
||||
@ -0,0 +1,221 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
private let compactNameFont = Font.regular(28.0)
|
||||
private let regularNameFont = Font.regular(36.0)
|
||||
|
||||
private let compactStatusFont = Font.regular(18.0)
|
||||
private let regularStatusFont = Font.regular(18.0)
|
||||
|
||||
enum LegacyCallControllerStatusValue: Equatable {
|
||||
case text(String)
|
||||
case timer((String) -> String, Double)
|
||||
|
||||
static func ==(lhs: LegacyCallControllerStatusValue, rhs: LegacyCallControllerStatusValue) -> Bool {
|
||||
switch lhs {
|
||||
case let .text(text):
|
||||
if case .text(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .timer(_, referenceTime):
|
||||
if case .timer(_, referenceTime) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class LegacyCallControllerStatusNode: ASDisplayNode {
|
||||
private let titleNode: TextNode
|
||||
private let statusNode: TextNode
|
||||
private let statusMeasureNode: TextNode
|
||||
private let receptionNode: LegacyCallControllerReceptionNode
|
||||
|
||||
var title: String = ""
|
||||
var subtitle: String = ""
|
||||
var status: LegacyCallControllerStatusValue = .text("") {
|
||||
didSet {
|
||||
if self.status != oldValue {
|
||||
self.statusTimer?.invalidate()
|
||||
|
||||
if case .timer = self.status {
|
||||
self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
|
||||
if let strongSelf = self, let validLayoutWidth = strongSelf.validLayoutWidth {
|
||||
let _ = strongSelf.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.statusTimer?.start()
|
||||
} else {
|
||||
if let validLayoutWidth = self.validLayoutWidth {
|
||||
let _ = self.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var reception: Int32? {
|
||||
didSet {
|
||||
if self.reception != oldValue {
|
||||
if let reception = self.reception {
|
||||
self.receptionNode.reception = reception
|
||||
|
||||
if oldValue == nil {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
|
||||
transition.updateAlpha(node: self.receptionNode, alpha: 1.0)
|
||||
}
|
||||
} else if self.reception == nil, oldValue != nil {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
|
||||
transition.updateAlpha(node: self.receptionNode, alpha: 0.0)
|
||||
}
|
||||
|
||||
if (oldValue == nil) != (self.reception != nil) {
|
||||
if let validLayoutWidth = self.validLayoutWidth {
|
||||
let _ = self.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var statusTimer: SwiftSignalKit.Timer?
|
||||
private var validLayoutWidth: CGFloat?
|
||||
|
||||
override init() {
|
||||
self.titleNode = TextNode()
|
||||
self.statusNode = TextNode()
|
||||
self.statusNode.displaysAsynchronously = false
|
||||
self.statusMeasureNode = TextNode()
|
||||
|
||||
self.receptionNode = LegacyCallControllerReceptionNode()
|
||||
self.receptionNode.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.statusNode)
|
||||
self.addSubnode(self.receptionNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.statusTimer?.invalidate()
|
||||
}
|
||||
|
||||
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayoutWidth = constrainedWidth
|
||||
|
||||
let nameFont: UIFont
|
||||
let statusFont: UIFont
|
||||
if constrainedWidth < 330.0 {
|
||||
nameFont = compactNameFont
|
||||
statusFont = compactStatusFont
|
||||
} else {
|
||||
nameFont = regularNameFont
|
||||
statusFont = regularStatusFont
|
||||
}
|
||||
|
||||
var statusOffset: CGFloat = 0.0
|
||||
let statusText: String
|
||||
let statusMeasureText: String
|
||||
switch self.status {
|
||||
case let .text(text):
|
||||
statusText = text
|
||||
statusMeasureText = text
|
||||
case let .timer(format, referenceTime):
|
||||
let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime)
|
||||
let durationString: String
|
||||
let measureDurationString: String
|
||||
if duration > 60 * 60 {
|
||||
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
|
||||
measureDurationString = "00:00:00"
|
||||
} else {
|
||||
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
|
||||
measureDurationString = "00:00"
|
||||
}
|
||||
statusText = format(durationString)
|
||||
statusMeasureText = format(measureDurationString)
|
||||
if self.reception != nil {
|
||||
statusOffset += 8.0
|
||||
}
|
||||
}
|
||||
|
||||
let spacing: CGFloat = 4.0
|
||||
let (titleLayout, titleApply) = TextNode.asyncLayout(self.titleNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: nameFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
|
||||
let (statusMeasureLayout, statusMeasureApply) = TextNode.asyncLayout(self.statusMeasureNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusMeasureText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
|
||||
let (statusLayout, statusApply) = TextNode.asyncLayout(self.statusNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
|
||||
|
||||
let _ = titleApply()
|
||||
let _ = statusApply()
|
||||
let _ = statusMeasureApply()
|
||||
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - titleLayout.size.width) / 2.0), y: 0.0), size: titleLayout.size)
|
||||
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0) + statusOffset, y: titleLayout.size.height + spacing), size: statusLayout.size)
|
||||
self.receptionNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX - receptionNodeSize.width, y: titleLayout.size.height + spacing + 9.0), size: receptionNodeSize)
|
||||
|
||||
return titleLayout.size.height + spacing + statusLayout.size.height
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class CallControllerReceptionNodeParameters: NSObject {
|
||||
let reception: Int32
|
||||
|
||||
init(reception: Int32) {
|
||||
self.reception = reception
|
||||
}
|
||||
}
|
||||
|
||||
private let receptionNodeSize = CGSize(width: 24.0, height: 10.0)
|
||||
|
||||
final class LegacyCallControllerReceptionNode : ASDisplayNode {
|
||||
var reception: Int32 = 4 {
|
||||
didSet {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.isOpaque = false
|
||||
self.isLayerBacked = true
|
||||
}
|
||||
|
||||
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
return CallControllerReceptionNodeParameters(reception: self.reception)
|
||||
}
|
||||
|
||||
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
|
||||
if let parameters = parameters as? CallControllerReceptionNodeParameters{
|
||||
let width: CGFloat = 3.0
|
||||
var spacing: CGFloat = 1.5
|
||||
if UIScreenScale > 2 {
|
||||
spacing = 4.0 / 3.0
|
||||
}
|
||||
|
||||
for i in 0 ..< 4 {
|
||||
let height = 4.0 + 2.0 * CGFloat(i)
|
||||
let rect = CGRect(x: bounds.minX + CGFloat(i) * (width + spacing), y: receptionNodeSize.height - height, width: width, height: height)
|
||||
|
||||
if i >= parameters.reception {
|
||||
context.setAlpha(0.4)
|
||||
}
|
||||
|
||||
let path = UIBezierPath(roundedRect: rect, cornerRadius: 0.5)
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -758,12 +758,34 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
return self.debugInfoValue.get()
|
||||
}
|
||||
|
||||
public func makeIncomingVideoView(completion: @escaping (UIView?) -> Void) {
|
||||
self.ongoingContext?.makeIncomingVideoView(completion: completion)
|
||||
public func makeIncomingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) {
|
||||
self.ongoingContext?.makeIncomingVideoView(completion: { view in
|
||||
if let view = view {
|
||||
completion(PresentationCallVideoView(
|
||||
view: view,
|
||||
setOnFirstFrameReceived: { [weak view] f in
|
||||
view?.setOnFirstFrameReceived(f)
|
||||
}
|
||||
))
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func makeOutgoingVideoView(completion: @escaping (UIView?) -> Void) {
|
||||
self.videoCapturer?.makeOutgoingVideoView(completion: completion)
|
||||
public func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) {
|
||||
self.videoCapturer?.makeOutgoingVideoView(completion: { view in
|
||||
if let view = view {
|
||||
completion(PresentationCallVideoView(
|
||||
view: view,
|
||||
setOnFirstFrameReceived: { [weak view] f in
|
||||
view?.setOnFirstFrameReceived(f)
|
||||
}
|
||||
))
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func switchVideoCamera() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -676,9 +676,13 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
|
||||
mainWindow.inCallNavigate = { [weak self] in
|
||||
if let strongSelf = self, let callController = strongSelf.callController {
|
||||
if callController.isNodeLoaded && callController.view.superview == nil {
|
||||
if callController.isNodeLoaded {
|
||||
mainWindow.hostView.containerView.endEditing(true)
|
||||
mainWindow.present(callController, on: .calls)
|
||||
if callController.view.superview == nil {
|
||||
mainWindow.present(callController, on: .calls)
|
||||
} else {
|
||||
callController.expandFromPipIfPossible()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,6 +111,12 @@ public final class TelegramRootController: NavigationController {
|
||||
}
|
||||
|
||||
let accountSettingsController = PeerInfoScreen(context: self.context, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, callMessages: [], isSettings: true)
|
||||
accountSettingsController.tabBarItemDebugTapAction = { [weak self, weak accountSettingsController] in
|
||||
guard let strongSelf = self, let accountSettingsController = accountSettingsController else {
|
||||
return
|
||||
}
|
||||
accountSettingsController.push(debugController(sharedContext: strongSelf.context.sharedContext, context: strongSelf.context))
|
||||
}
|
||||
controllers.append(accountSettingsController)
|
||||
|
||||
tabBarController.setControllers(controllers, selectedIndex: restoreSettignsController != nil ? (controllers.count - 1) : (controllers.count - 2))
|
||||
|
||||
@ -104,8 +104,16 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
|
||||
private var initializedStatus = false
|
||||
private let _status = Promise<MediaPlayerStatus>()
|
||||
private let _thumbnailStatus = Promise<MediaPlayerStatus?>(nil)
|
||||
var status: Signal<MediaPlayerStatus, NoError> {
|
||||
return self._status.get()
|
||||
return combineLatest(self._thumbnailStatus.get(), self._status.get())
|
||||
|> map { thumbnailStatus, status in
|
||||
if let thumbnailStatus = thumbnailStatus {
|
||||
return thumbnailStatus
|
||||
} else {
|
||||
return status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let _bufferingStatus = Promise<(IndexSet, Int)?>()
|
||||
@ -183,7 +191,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.playerNode)
|
||||
self._status.set(combineLatest(self.dimensionsPromise.get(), self.player.status)
|
||||
|> map { [weak self] dimensions, status in
|
||||
|> map { dimensions, status in
|
||||
return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled)
|
||||
})
|
||||
|
||||
@ -249,6 +257,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
self.thumbnailNode = thumbnailNode
|
||||
thumbnailPlayer.attachPlayerNode(thumbnailNode)
|
||||
|
||||
self._thumbnailStatus.set(thumbnailPlayer.status
|
||||
|> map { status in
|
||||
return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: CGSize(), timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled)
|
||||
})
|
||||
|
||||
self.addSubnode(thumbnailNode)
|
||||
|
||||
thumbnailNode.frame = self.playerNode.frame
|
||||
|
||||
@ -302,7 +302,7 @@ public final class OngoingCallVideoCapturer {
|
||||
self.impl.switchVideoCamera()
|
||||
}
|
||||
|
||||
public func makeOutgoingVideoView(completion: @escaping (UIView?) -> Void) {
|
||||
public func makeOutgoingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) {
|
||||
self.impl.makeOutgoingVideoView(completion)
|
||||
}
|
||||
|
||||
@ -418,6 +418,10 @@ private extension OngoingCallContextState.State {
|
||||
}
|
||||
}*/
|
||||
|
||||
public protocol OngoingCallContextPresentationCallVideoView: UIView {
|
||||
func setOnFirstFrameReceived(_ onFirstFrameReceived: (() -> Void)?)
|
||||
}
|
||||
|
||||
public final class OngoingCallContext {
|
||||
public struct AuxiliaryServer {
|
||||
public enum Connection {
|
||||
@ -725,7 +729,7 @@ public final class OngoingCallContext {
|
||||
return (poll |> then(.complete() |> delay(0.5, queue: Queue.concurrentDefaultQueue()))) |> restart
|
||||
}
|
||||
|
||||
public func makeIncomingVideoView(completion: @escaping (UIView?) -> Void) {
|
||||
public func makeIncomingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) {
|
||||
self.withContext { context in
|
||||
if let context = context as? OngoingCallThreadLocalContextWebrtc {
|
||||
context.makeIncomingVideoView(completion)
|
||||
@ -735,3 +739,6 @@ public final class OngoingCallContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OngoingCallThreadLocalContextWebrtcVideoView: OngoingCallContextPresentationCallVideoView {
|
||||
}
|
||||
|
||||
@ -195,11 +195,19 @@ std::unique_ptr<webrtc::VideoDecoderFactory> makeVideoDecoderFactory() {
|
||||
}
|
||||
|
||||
bool supportsH265Encoding() {
|
||||
#if TARGET_OS_IOS
|
||||
if (@available(iOS 11.0, *)) {
|
||||
return [[AVAssetExportSession allExportPresets] containsObject:AVAssetExportPresetHEVCHighestQuality];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (@available(macOS 10.13, *)) {
|
||||
return [[AVAssetExportSession allExportPresets] containsObject:AVAssetExportPresetHEVCHighestQuality];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> makeVideoSource(rtc::Thread *signalingThread, rtc::Thread *workerThread) {
|
||||
|
||||
@ -5,12 +5,13 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "api/media_stream_interface.h"
|
||||
#import <TgVoip/OngoingCallThreadLocalContext.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
@class RTCVideoFrame;
|
||||
|
||||
@interface VideoMetalView : UIView
|
||||
@interface VideoMetalView : OngoingCallThreadLocalContextWebrtcVideoView
|
||||
|
||||
@property(nonatomic) UIViewContentMode videoContentMode;
|
||||
@property(nonatomic, getter=isEnabled) BOOL enabled;
|
||||
|
||||
@ -52,6 +52,9 @@ private:
|
||||
|
||||
CGSize _currentSize;
|
||||
std::shared_ptr<VideoRendererAdapterImpl> _sink;
|
||||
|
||||
void (^_onFirstFrameReceived)();
|
||||
bool _firstFrameReceivedReported;
|
||||
}
|
||||
|
||||
@end
|
||||
@ -264,6 +267,11 @@ private:
|
||||
|
||||
- (void)renderFrame:(nullable RTCVideoFrame *)frame {
|
||||
assert([NSThread isMainThread]);
|
||||
|
||||
if (!_firstFrameReceivedReported && _onFirstFrameReceived) {
|
||||
_firstFrameReceivedReported = true;
|
||||
_onFirstFrameReceived();
|
||||
}
|
||||
|
||||
if (!self.isEnabled) {
|
||||
return;
|
||||
@ -282,4 +290,9 @@ private:
|
||||
return _sink;
|
||||
}
|
||||
|
||||
- (void)setOnFirstFrameReceived:(void (^ _Nullable)())onFirstFrameReceived {
|
||||
_onFirstFrameReceived = [onFirstFrameReceived copy];
|
||||
_firstFrameReceivedReported = false;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@ -78,6 +78,12 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
|
||||
|
||||
@end
|
||||
|
||||
@interface OngoingCallThreadLocalContextWebrtcVideoView : UIView
|
||||
|
||||
- (void)setOnFirstFrameReceived:(void (^ _Nullable)())onFirstFrameReceived;
|
||||
|
||||
@end
|
||||
|
||||
@interface OngoingCallThreadLocalContextVideoCapturer : NSObject
|
||||
|
||||
- (instancetype _Nonnull)init;
|
||||
@ -85,7 +91,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
|
||||
- (void)switchVideoCamera;
|
||||
- (void)setIsVideoEnabled:(bool)isVideoEnabled;
|
||||
|
||||
- (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion;
|
||||
- (void)makeOutgoingVideoView:(void (^_Nonnull)(OngoingCallThreadLocalContextWebrtcVideoView * _Nullable))completion;
|
||||
|
||||
@end
|
||||
|
||||
@ -111,7 +117,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
|
||||
- (void)setIsMuted:(bool)isMuted;
|
||||
- (void)setVideoEnabled:(bool)videoEnabled;
|
||||
- (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType;
|
||||
- (void)makeIncomingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion;
|
||||
- (void)makeIncomingVideoView:(void (^_Nonnull)(OngoingCallThreadLocalContextWebrtcVideoView * _Nullable))completion;
|
||||
- (void)addSignalingData:(NSData * _Nonnull)data;
|
||||
|
||||
@end
|
||||
|
||||
@ -49,7 +49,7 @@ using namespace TGVOIP_NAMESPACE;
|
||||
return _interface;
|
||||
}
|
||||
|
||||
- (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion {
|
||||
- (void)makeOutgoingVideoView:(void (^_Nonnull)(OngoingCallThreadLocalContextWebrtcVideoView * _Nullable))completion {
|
||||
std::shared_ptr<TgVoipVideoCaptureInterface> interface = _interface;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
VideoMetalView *remoteRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero];
|
||||
@ -478,7 +478,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)makeIncomingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion {
|
||||
- (void)makeIncomingVideoView:(void (^_Nonnull)(OngoingCallThreadLocalContextWebrtcVideoView * _Nullable))completion {
|
||||
if (_tgVoip) {
|
||||
__weak OngoingCallThreadLocalContextWebrtc *weakSelf = self;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@ -498,3 +498,9 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OngoingCallThreadLocalContextWebrtcVideoView : UIView
|
||||
|
||||
- (void)setOnFirstFrameReceived:(void (^ _Nullable)())onFirstFrameReceived {
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Binary file not shown.
@ -449,12 +449,12 @@ public final class WalletStrings: Equatable {
|
||||
public var Wallet_SecureStorageReset_Title: String { return self._s[219]! }
|
||||
public var Wallet_Receive_CommentHeader: String { return self._s[220]! }
|
||||
public var Wallet_Info_ReceiveGrams: String { return self._s[221]! }
|
||||
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
|
||||
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
|
||||
let form = getPluralizationForm(self.lc, value)
|
||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
|
||||
}
|
||||
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
|
||||
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
|
||||
let form = getPluralizationForm(self.lc, value)
|
||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user