Merge branch 'master' into beta
@ -73,6 +73,7 @@ public protocol PresentationCall: class {
|
|||||||
var internalId: CallSessionInternalId { get }
|
var internalId: CallSessionInternalId { get }
|
||||||
var peerId: PeerId { get }
|
var peerId: PeerId { get }
|
||||||
var isOutgoing: Bool { get }
|
var isOutgoing: Bool { get }
|
||||||
|
var isVideo: Bool { get }
|
||||||
var peer: Peer? { get }
|
var peer: Peer? { get }
|
||||||
|
|
||||||
var state: Signal<PresentationCallState, NoError> { get }
|
var state: Signal<PresentationCallState, NoError> { get }
|
||||||
|
@ -54,7 +54,13 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||||||
peer = chatPeer.chatMainPeer
|
peer = chatPeer.chatMainPeer
|
||||||
}
|
}
|
||||||
|
|
||||||
messageText = messages[0].text
|
messageText = ""
|
||||||
|
for message in messages {
|
||||||
|
if !message.text.isEmpty {
|
||||||
|
messageText = message.text
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var textIsReady = false
|
var textIsReady = false
|
||||||
if messages.count > 1 {
|
if messages.count > 1 {
|
||||||
|
@ -133,7 +133,14 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U
|
|||||||
|
|
||||||
var samplePtr = bufferData.advanced(by: dataOffset).assumingMemoryBound(to: Int16.self)
|
var samplePtr = bufferData.advanced(by: dataOffset).assumingMemoryBound(to: Int16.self)
|
||||||
for _ in 0 ..< actualConsumedCount / 4 {
|
for _ in 0 ..< actualConsumedCount / 4 {
|
||||||
let sample: Int16 = abs(samplePtr.pointee)
|
var sample: Int16 = samplePtr.pointee
|
||||||
|
if sample < 0 {
|
||||||
|
if sample <= -32768 {
|
||||||
|
sample = Int16.max
|
||||||
|
} else {
|
||||||
|
sample = -sample
|
||||||
|
}
|
||||||
|
}
|
||||||
samplePtr = samplePtr.advanced(by: 2)
|
samplePtr = samplePtr.advanced(by: 2)
|
||||||
|
|
||||||
if context.audioLevelPeak < sample {
|
if context.audioLevelPeak < sample {
|
||||||
|
@ -14,9 +14,33 @@ import AccountContext
|
|||||||
import TelegramNotices
|
import TelegramNotices
|
||||||
import AppBundle
|
import AppBundle
|
||||||
|
|
||||||
|
protocol CallControllerNodeProtocol: class {
|
||||||
|
var isMuted: Bool { get set }
|
||||||
|
|
||||||
|
var toggleMute: (() -> Void)? { get set }
|
||||||
|
var setCurrentAudioOutput: ((AudioSessionOutput) -> Void)? { get set }
|
||||||
|
var beginAudioOuputSelection: (() -> Void)? { get set }
|
||||||
|
var acceptCall: (() -> Void)? { get set }
|
||||||
|
var endCall: (() -> Void)? { get set }
|
||||||
|
var setIsVideoPaused: ((Bool) -> Void)? { get set }
|
||||||
|
var back: (() -> Void)? { get set }
|
||||||
|
var presentCallRating: ((CallId) -> Void)? { get set }
|
||||||
|
var callEnded: ((Bool) -> Void)? { get set }
|
||||||
|
var dismissedInteractively: (() -> Void)? { get set }
|
||||||
|
|
||||||
|
func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?)
|
||||||
|
func updateCallState(_ callState: PresentationCallState)
|
||||||
|
func updatePeer(accountPeer: Peer, peer: Peer, hasOther: Bool)
|
||||||
|
|
||||||
|
func animateIn()
|
||||||
|
func animateOut(completion: @escaping () -> Void)
|
||||||
|
|
||||||
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition)
|
||||||
|
}
|
||||||
|
|
||||||
public final class CallController: ViewController {
|
public final class CallController: ViewController {
|
||||||
private var controllerNode: CallControllerNode {
|
private var controllerNode: CallControllerNodeProtocol {
|
||||||
return self.displayNode as! CallControllerNode
|
return self.displayNode as! CallControllerNodeProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
private let _ready = Promise<Bool>(false)
|
private let _ready = Promise<Bool>(false)
|
||||||
@ -109,7 +133,11 @@ public final class CallController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override public func loadDisplayNode() {
|
override public func loadDisplayNode() {
|
||||||
|
if self.call.isVideo {
|
||||||
self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
|
self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
|
||||||
|
} else {
|
||||||
|
self.displayNode = LegacyCallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
|
||||||
|
}
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
self.controllerNode.toggleMute = { [weak self] in
|
self.controllerNode.toggleMute = { [weak self] in
|
||||||
|
@ -133,7 +133,7 @@ private final class OutgoingVideoNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class CallControllerNode: ASDisplayNode {
|
final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||||
private enum VideoNodeCorner {
|
private enum VideoNodeCorner {
|
||||||
case topLeft
|
case topLeft
|
||||||
case topRight
|
case topRight
|
||||||
|
@ -0,0 +1,249 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
|
enum LegacyCallControllerButtonType {
|
||||||
|
case mute
|
||||||
|
case end
|
||||||
|
case accept
|
||||||
|
case speaker
|
||||||
|
case bluetooth
|
||||||
|
case switchCamera
|
||||||
|
}
|
||||||
|
|
||||||
|
private let buttonSize = CGSize(width: 75.0, height: 75.0)
|
||||||
|
|
||||||
|
private func generateEmptyButtonImage(icon: UIImage?, strokeColor: UIColor?, fillColor: UIColor, knockout: Bool = false, angle: CGFloat = 0.0) -> UIImage? {
|
||||||
|
return generateImage(buttonSize, contextGenerator: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setBlendMode(.copy)
|
||||||
|
if let strokeColor = strokeColor {
|
||||||
|
context.setFillColor(strokeColor.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setFillColor(fillColor.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: 1.5, y: 1.5), size: CGSize(width: size.width - 3.0, height: size.height - 3.0)))
|
||||||
|
} else {
|
||||||
|
context.setFillColor(fillColor.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let icon = icon {
|
||||||
|
if !angle.isZero {
|
||||||
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||||
|
context.rotate(by: angle)
|
||||||
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||||
|
}
|
||||||
|
let imageSize = icon.size
|
||||||
|
let imageRect = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.width - imageSize.height) / 2.0)), size: imageSize)
|
||||||
|
if knockout {
|
||||||
|
context.setBlendMode(.copy)
|
||||||
|
context.clip(to: imageRect, mask: icon.cgImage!)
|
||||||
|
context.setFillColor(UIColor.clear.cgColor)
|
||||||
|
context.fill(imageRect)
|
||||||
|
} else {
|
||||||
|
context.setBlendMode(.normal)
|
||||||
|
context.draw(icon.cgImage!, in: imageRect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateFilledButtonImage(color: UIColor, icon: UIImage?, angle: CGFloat = 0.0) -> UIImage? {
|
||||||
|
return generateImage(buttonSize, contextGenerator: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setBlendMode(.normal)
|
||||||
|
context.setFillColor(color.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
|
if let icon = icon {
|
||||||
|
if !angle.isZero {
|
||||||
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||||
|
context.rotate(by: angle)
|
||||||
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||||
|
}
|
||||||
|
context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - icon.size.width) / 2.0), y: floor((size.height - icon.size.height) / 2.0)), size: icon.size))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private let emptyStroke = UIColor(white: 1.0, alpha: 0.8)
|
||||||
|
private let emptyHighlightedFill = UIColor(white: 1.0, alpha: 0.3)
|
||||||
|
private let invertedFill = UIColor(white: 1.0, alpha: 1.0)
|
||||||
|
|
||||||
|
private let labelFont = Font.regular(14.5)
|
||||||
|
|
||||||
|
final class LegacyCallControllerButtonNode: HighlightTrackingButtonNode {
|
||||||
|
private var type: LegacyCallControllerButtonType
|
||||||
|
|
||||||
|
private var regularImage: UIImage?
|
||||||
|
private var highlightedImage: UIImage?
|
||||||
|
private var filledImage: UIImage?
|
||||||
|
|
||||||
|
private let backgroundNode: ASImageNode
|
||||||
|
private let labelNode: ASTextNode?
|
||||||
|
|
||||||
|
init(type: LegacyCallControllerButtonType, label: String?) {
|
||||||
|
self.type = type
|
||||||
|
|
||||||
|
self.backgroundNode = ASImageNode()
|
||||||
|
self.backgroundNode.isLayerBacked = true
|
||||||
|
self.backgroundNode.displayWithoutProcessing = false
|
||||||
|
self.backgroundNode.displaysAsynchronously = false
|
||||||
|
|
||||||
|
if let label = label {
|
||||||
|
let labelNode = ASTextNode()
|
||||||
|
labelNode.attributedText = NSAttributedString(string: label, font: labelFont, textColor: .white)
|
||||||
|
self.labelNode = labelNode
|
||||||
|
} else {
|
||||||
|
self.labelNode = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var regularImage: UIImage?
|
||||||
|
var highlightedImage: UIImage?
|
||||||
|
var filledImage: UIImage?
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .mute:
|
||||||
|
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||||
|
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||||
|
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||||
|
case .accept:
|
||||||
|
regularImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0)
|
||||||
|
highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0)
|
||||||
|
case .end:
|
||||||
|
regularImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"))
|
||||||
|
highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"))
|
||||||
|
case .speaker:
|
||||||
|
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||||
|
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||||
|
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||||
|
case .bluetooth:
|
||||||
|
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||||
|
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||||
|
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||||
|
case .switchCamera:
|
||||||
|
let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: .white)
|
||||||
|
regularImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: .clear)
|
||||||
|
highlightedImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||||
|
filledImage = generateEmptyButtonImage(icon: patternImage, strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.regularImage = regularImage
|
||||||
|
self.highlightedImage = highlightedImage
|
||||||
|
self.filledImage = filledImage
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.backgroundNode)
|
||||||
|
|
||||||
|
if let labelNode = self.labelNode {
|
||||||
|
self.addSubnode(labelNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundNode.image = regularImage
|
||||||
|
self.currentImage = regularImage
|
||||||
|
|
||||||
|
self.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.internalHighlighted = highlighted
|
||||||
|
strongSelf.updateState(highlighted: highlighted, selected: strongSelf.isSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var internalHighlighted = false
|
||||||
|
|
||||||
|
override var isSelected: Bool {
|
||||||
|
didSet {
|
||||||
|
self.updateState(highlighted: self.internalHighlighted, selected: self.isSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentImage: UIImage?
|
||||||
|
|
||||||
|
private func updateState(highlighted: Bool, selected: Bool) {
|
||||||
|
let image: UIImage?
|
||||||
|
if selected {
|
||||||
|
image = self.filledImage
|
||||||
|
} else if highlighted {
|
||||||
|
image = self.highlightedImage
|
||||||
|
} else {
|
||||||
|
image = self.regularImage
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.currentImage !== image {
|
||||||
|
let currentContents = self.backgroundNode.layer.contents
|
||||||
|
self.backgroundNode.layer.removeAnimation(forKey: "contents")
|
||||||
|
if let currentContents = currentContents, let image = image {
|
||||||
|
self.backgroundNode.image = image
|
||||||
|
self.backgroundNode.layer.animate(from: currentContents as AnyObject, to: image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: image === self.currentImage || image === self.filledImage ? 0.25 : 0.15)
|
||||||
|
} else {
|
||||||
|
self.backgroundNode.image = image
|
||||||
|
}
|
||||||
|
self.currentImage = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateType(_ type: LegacyCallControllerButtonType) {
|
||||||
|
if self.type == type {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.type = type
|
||||||
|
var regularImage: UIImage?
|
||||||
|
var highlightedImage: UIImage?
|
||||||
|
var filledImage: UIImage?
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .mute:
|
||||||
|
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||||
|
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||||
|
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||||
|
case .accept:
|
||||||
|
regularImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0)
|
||||||
|
highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0)
|
||||||
|
case .end:
|
||||||
|
regularImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"))
|
||||||
|
highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"))
|
||||||
|
case .speaker:
|
||||||
|
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||||
|
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||||
|
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||||
|
case .bluetooth:
|
||||||
|
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||||
|
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||||
|
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||||
|
case .switchCamera:
|
||||||
|
let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: .white)
|
||||||
|
regularImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: .clear)
|
||||||
|
highlightedImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||||
|
filledImage = generateEmptyButtonImage(icon: patternImage, strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.regularImage = regularImage
|
||||||
|
self.highlightedImage = highlightedImage
|
||||||
|
self.filledImage = filledImage
|
||||||
|
|
||||||
|
self.updateState(highlighted: self.isHighlighted, selected: self.isSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateRollTransition() {
|
||||||
|
self.backgroundNode.layer.animate(from: 0.0 as NSNumber, to: (-CGFloat.pi * 5 / 4) as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3, removeOnCompletion: false)
|
||||||
|
self.labelNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layout() {
|
||||||
|
super.layout()
|
||||||
|
|
||||||
|
let size = self.bounds.size
|
||||||
|
|
||||||
|
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))
|
||||||
|
|
||||||
|
if let labelNode = self.labelNode {
|
||||||
|
let labelSize = labelNode.measure(CGSize(width: 200.0, height: 100.0))
|
||||||
|
labelNode.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: 81.0), size: labelSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,261 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import MediaPlayer
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
enum LegacyCallControllerButtonsSpeakerMode {
|
||||||
|
case none
|
||||||
|
case builtin
|
||||||
|
case speaker
|
||||||
|
case headphones
|
||||||
|
case bluetooth
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LegacyCallControllerButtonsMode: Equatable {
|
||||||
|
enum VideoState: Equatable {
|
||||||
|
case notAvailable
|
||||||
|
case available(Bool)
|
||||||
|
case active
|
||||||
|
}
|
||||||
|
|
||||||
|
case active(speakerMode: LegacyCallControllerButtonsSpeakerMode, videoState: VideoState)
|
||||||
|
case incoming
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LegacyCallControllerButtonsNode: ASDisplayNode {
|
||||||
|
private let acceptButton: LegacyCallControllerButtonNode
|
||||||
|
private let declineButton: LegacyCallControllerButtonNode
|
||||||
|
|
||||||
|
private let muteButton: LegacyCallControllerButtonNode
|
||||||
|
private let endButton: LegacyCallControllerButtonNode
|
||||||
|
private let speakerButton: LegacyCallControllerButtonNode
|
||||||
|
private let swichCameraButton: LegacyCallControllerButtonNode
|
||||||
|
|
||||||
|
private var mode: LegacyCallControllerButtonsMode?
|
||||||
|
|
||||||
|
private var validLayout: CGFloat?
|
||||||
|
|
||||||
|
var isMuted = false {
|
||||||
|
didSet {
|
||||||
|
self.muteButton.isSelected = self.isMuted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var accept: (() -> Void)?
|
||||||
|
var mute: (() -> Void)?
|
||||||
|
var end: (() -> Void)?
|
||||||
|
var speaker: (() -> Void)?
|
||||||
|
var toggleVideo: (() -> Void)?
|
||||||
|
var rotateCamera: (() -> Void)?
|
||||||
|
|
||||||
|
init(strings: PresentationStrings) {
|
||||||
|
self.acceptButton = LegacyCallControllerButtonNode(type: .accept, label: strings.Call_Accept)
|
||||||
|
self.acceptButton.alpha = 0.0
|
||||||
|
self.declineButton = LegacyCallControllerButtonNode(type: .end, label: strings.Call_Decline)
|
||||||
|
self.declineButton.alpha = 0.0
|
||||||
|
|
||||||
|
self.muteButton = LegacyCallControllerButtonNode(type: .mute, label: nil)
|
||||||
|
self.muteButton.alpha = 0.0
|
||||||
|
self.endButton = LegacyCallControllerButtonNode(type: .end, label: nil)
|
||||||
|
self.endButton.alpha = 0.0
|
||||||
|
self.speakerButton = LegacyCallControllerButtonNode(type: .speaker, label: nil)
|
||||||
|
self.speakerButton.alpha = 0.0
|
||||||
|
self.swichCameraButton = LegacyCallControllerButtonNode(type: .switchCamera, label: nil)
|
||||||
|
self.swichCameraButton.alpha = 0.0
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.acceptButton)
|
||||||
|
self.addSubnode(self.declineButton)
|
||||||
|
self.addSubnode(self.muteButton)
|
||||||
|
self.addSubnode(self.endButton)
|
||||||
|
self.addSubnode(self.speakerButton)
|
||||||
|
self.addSubnode(self.swichCameraButton)
|
||||||
|
|
||||||
|
self.acceptButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||||
|
self.declineButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||||
|
self.muteButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||||
|
self.endButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||||
|
self.speakerButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||||
|
self.swichCameraButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
|
let previousLayout = self.validLayout
|
||||||
|
self.validLayout = constrainedWidth
|
||||||
|
|
||||||
|
if let mode = self.mode, previousLayout != self.validLayout {
|
||||||
|
self.updateButtonsLayout(mode: mode, width: constrainedWidth, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMode(_ mode: LegacyCallControllerButtonsMode) {
|
||||||
|
if self.mode != mode {
|
||||||
|
let previousMode = self.mode
|
||||||
|
self.mode = mode
|
||||||
|
if let validLayout = self.validLayout {
|
||||||
|
self.updateButtonsLayout(mode: mode, width: validLayout, animated: previousMode != nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateButtonsLayout(mode: LegacyCallControllerButtonsMode, width: CGFloat, animated: Bool) {
|
||||||
|
let transition: ContainedViewLayoutTransition
|
||||||
|
if animated {
|
||||||
|
transition = .animated(duration: 0.3, curve: .spring)
|
||||||
|
} else {
|
||||||
|
transition = .immediate
|
||||||
|
}
|
||||||
|
|
||||||
|
let threeButtonSpacing: CGFloat = 28.0
|
||||||
|
let twoButtonSpacing: CGFloat = 105.0
|
||||||
|
let buttonSize = CGSize(width: 75.0, height: 75.0)
|
||||||
|
|
||||||
|
let threeButtonsWidth = 3.0 * buttonSize.width + 2.0 * threeButtonSpacing
|
||||||
|
let twoButtonsWidth = 2.0 * buttonSize.width + 1.0 * twoButtonSpacing
|
||||||
|
|
||||||
|
var origin = CGPoint(x: floor((width - threeButtonsWidth) / 2.0), y: 0.0)
|
||||||
|
|
||||||
|
for button in [self.muteButton, self.endButton, self.speakerButton] {
|
||||||
|
transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize))
|
||||||
|
if button === self.speakerButton {
|
||||||
|
transition.updateFrame(node: self.swichCameraButton, frame: CGRect(origin: origin, size: buttonSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
origin.x += buttonSize.width + threeButtonSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
origin = CGPoint(x: floor((width - twoButtonsWidth) / 2.0), y: 0.0)
|
||||||
|
for button in [self.declineButton, self.acceptButton] {
|
||||||
|
transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize))
|
||||||
|
origin.x += buttonSize.width + twoButtonSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case .incoming:
|
||||||
|
for button in [self.declineButton, self.acceptButton] {
|
||||||
|
button.alpha = 1.0
|
||||||
|
}
|
||||||
|
for button in [self.muteButton, self.endButton, self.speakerButton, self.swichCameraButton] {
|
||||||
|
button.alpha = 0.0
|
||||||
|
}
|
||||||
|
case let .active(speakerMode, videoState):
|
||||||
|
for button in [self.muteButton] {
|
||||||
|
if animated && button.alpha.isZero {
|
||||||
|
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
|
}
|
||||||
|
button.alpha = 1.0
|
||||||
|
}
|
||||||
|
switch videoState {
|
||||||
|
case .active, .available:
|
||||||
|
for button in [self.speakerButton] {
|
||||||
|
if animated && !button.alpha.isZero {
|
||||||
|
button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||||
|
}
|
||||||
|
button.alpha = 0.0
|
||||||
|
}
|
||||||
|
for button in [self.swichCameraButton] {
|
||||||
|
if animated && button.alpha.isZero {
|
||||||
|
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
|
}
|
||||||
|
button.alpha = 1.0
|
||||||
|
}
|
||||||
|
case .notAvailable:
|
||||||
|
for button in [self.swichCameraButton] {
|
||||||
|
if animated && !button.alpha.isZero {
|
||||||
|
button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||||
|
}
|
||||||
|
button.alpha = 0.0
|
||||||
|
}
|
||||||
|
for button in [self.speakerButton] {
|
||||||
|
if animated && button.alpha.isZero {
|
||||||
|
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
|
}
|
||||||
|
button.alpha = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var animatingAcceptButton = false
|
||||||
|
if self.endButton.alpha.isZero {
|
||||||
|
if animated {
|
||||||
|
if !self.acceptButton.alpha.isZero {
|
||||||
|
animatingAcceptButton = true
|
||||||
|
self.endButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
self.acceptButton.animateRollTransition()
|
||||||
|
self.endButton.layer.animate(from: (CGFloat.pi * 5 / 4) as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3)
|
||||||
|
self.acceptButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.acceptButton.alpha = 0.0
|
||||||
|
strongSelf.acceptButton.layer.removeAnimation(forKey: "position")
|
||||||
|
strongSelf.acceptButton.layer.removeAnimation(forKey: "transform.rotation.z")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.endButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
self.endButton.alpha = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.declineButton.alpha.isZero {
|
||||||
|
if animated {
|
||||||
|
self.declineButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
self.declineButton.alpha = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.acceptButton.alpha.isZero && !animatingAcceptButton {
|
||||||
|
self.acceptButton.alpha = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.speakerButton.isSelected = speakerMode == .speaker
|
||||||
|
self.speakerButton.isHidden = speakerMode == .none
|
||||||
|
let speakerButtonType: LegacyCallControllerButtonType
|
||||||
|
switch speakerMode {
|
||||||
|
case .none, .builtin, .speaker:
|
||||||
|
speakerButtonType = .speaker
|
||||||
|
case .headphones:
|
||||||
|
speakerButtonType = .bluetooth
|
||||||
|
case .bluetooth:
|
||||||
|
speakerButtonType = .bluetooth
|
||||||
|
}
|
||||||
|
self.speakerButton.updateType(speakerButtonType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func buttonPressed(_ button: LegacyCallControllerButtonNode) {
|
||||||
|
if button === self.muteButton {
|
||||||
|
self.mute?()
|
||||||
|
} else if button === self.endButton || button === self.declineButton {
|
||||||
|
self.end?()
|
||||||
|
} else if button === self.speakerButton {
|
||||||
|
self.speaker?()
|
||||||
|
} else if button === self.acceptButton {
|
||||||
|
self.accept?()
|
||||||
|
} else if button === self.swichCameraButton {
|
||||||
|
self.rotateCamera?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
let buttons = [
|
||||||
|
self.acceptButton,
|
||||||
|
self.declineButton,
|
||||||
|
self.muteButton,
|
||||||
|
self.endButton,
|
||||||
|
self.speakerButton,
|
||||||
|
self.swichCameraButton
|
||||||
|
]
|
||||||
|
for button in buttons {
|
||||||
|
if button.isHidden || button.alpha.isZero {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,787 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import SyncCore
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramPresentationData
|
||||||
|
import TelegramUIPreferences
|
||||||
|
import TelegramAudio
|
||||||
|
import AccountContext
|
||||||
|
import LocalizedPeerData
|
||||||
|
import PhotoResources
|
||||||
|
import CallsEmoji
|
||||||
|
|
||||||
|
private final class IncomingVideoNode: ASDisplayNode {
|
||||||
|
private let videoView: UIView
|
||||||
|
private var effectView: UIVisualEffectView?
|
||||||
|
private var isBlurred: Bool = false
|
||||||
|
|
||||||
|
init(videoView: UIView) {
|
||||||
|
self.videoView = videoView
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.view.addSubview(self.videoView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(size: CGSize) {
|
||||||
|
self.videoView.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateIsBlurred(isBlurred: Bool) {
|
||||||
|
if self.isBlurred == isBlurred {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isBlurred = isBlurred
|
||||||
|
|
||||||
|
if isBlurred {
|
||||||
|
if self.effectView == nil {
|
||||||
|
let effectView = UIVisualEffectView()
|
||||||
|
self.effectView = effectView
|
||||||
|
effectView.frame = self.videoView.frame
|
||||||
|
self.view.addSubview(effectView)
|
||||||
|
}
|
||||||
|
UIView.animate(withDuration: 0.3, animations: {
|
||||||
|
self.effectView?.effect = UIBlurEffect(style: .dark)
|
||||||
|
})
|
||||||
|
} else if let effectView = self.effectView {
|
||||||
|
UIView.animate(withDuration: 0.3, animations: {
|
||||||
|
effectView.effect = nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class OutgoingVideoNode: ASDisplayNode {
|
||||||
|
private let videoView: UIView
|
||||||
|
private let switchCameraButton: HighlightableButtonNode
|
||||||
|
private let switchCamera: () -> Void
|
||||||
|
|
||||||
|
init(videoView: UIView, switchCamera: @escaping () -> Void) {
|
||||||
|
self.videoView = videoView
|
||||||
|
self.switchCameraButton = HighlightableButtonNode()
|
||||||
|
self.switchCamera = switchCamera
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.view.addSubview(self.videoView)
|
||||||
|
self.addSubnode(self.switchCameraButton)
|
||||||
|
self.switchCameraButton.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func buttonPressed() {
|
||||||
|
self.switchCamera()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(size: CGSize, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
|
transition.updateFrame(view: self.videoView, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
transition.updateCornerRadius(layer: self.videoView.layer, cornerRadius: isExpanded ? 0.0 : 16.0)
|
||||||
|
self.switchCameraButton.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
||||||
|
private let sharedContext: SharedAccountContext
|
||||||
|
private let account: Account
|
||||||
|
|
||||||
|
private let statusBar: StatusBar
|
||||||
|
|
||||||
|
private var presentationData: PresentationData
|
||||||
|
private var peer: Peer?
|
||||||
|
private let debugInfo: Signal<(String, String), NoError>
|
||||||
|
private var forceReportRating = false
|
||||||
|
private let easyDebugAccess: Bool
|
||||||
|
private let call: PresentationCall
|
||||||
|
|
||||||
|
private let containerNode: ASDisplayNode
|
||||||
|
|
||||||
|
private let imageNode: TransformImageNode
|
||||||
|
private let dimNode: ASDisplayNode
|
||||||
|
private var incomingVideoNode: IncomingVideoNode?
|
||||||
|
private var incomingVideoViewRequested: Bool = false
|
||||||
|
private var outgoingVideoNode: OutgoingVideoNode?
|
||||||
|
private var outgoingVideoViewRequested: Bool = false
|
||||||
|
private let backButtonArrowNode: ASImageNode
|
||||||
|
private let backButtonNode: HighlightableButtonNode
|
||||||
|
private let statusNode: CallControllerStatusNode
|
||||||
|
private let videoPausedNode: ImmediateTextNode
|
||||||
|
private let buttonsNode: LegacyCallControllerButtonsNode
|
||||||
|
private var keyPreviewNode: CallControllerKeyPreviewNode?
|
||||||
|
|
||||||
|
private var debugNode: CallDebugNode?
|
||||||
|
|
||||||
|
private var keyTextData: (Data, String)?
|
||||||
|
private let keyButtonNode: HighlightableButtonNode
|
||||||
|
|
||||||
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||||
|
|
||||||
|
var isMuted: Bool = false {
|
||||||
|
didSet {
|
||||||
|
self.buttonsNode.isMuted = self.isMuted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shouldStayHiddenUntilConnection: Bool = false
|
||||||
|
|
||||||
|
private var audioOutputState: ([AudioSessionOutput], currentOutput: AudioSessionOutput?)?
|
||||||
|
private var callState: PresentationCallState?
|
||||||
|
|
||||||
|
var toggleMute: (() -> Void)?
|
||||||
|
var setCurrentAudioOutput: ((AudioSessionOutput) -> Void)?
|
||||||
|
var beginAudioOuputSelection: (() -> Void)?
|
||||||
|
var acceptCall: (() -> Void)?
|
||||||
|
var endCall: (() -> Void)?
|
||||||
|
var toggleVideo: (() -> Void)?
|
||||||
|
var back: (() -> Void)?
|
||||||
|
var presentCallRating: ((CallId) -> Void)?
|
||||||
|
var callEnded: ((Bool) -> Void)?
|
||||||
|
var dismissedInteractively: (() -> Void)?
|
||||||
|
var setIsVideoPaused: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
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
|
||||||
|
self.presentationData = presentationData
|
||||||
|
self.statusBar = statusBar
|
||||||
|
self.debugInfo = debugInfo
|
||||||
|
self.shouldStayHiddenUntilConnection = shouldStayHiddenUntilConnection
|
||||||
|
self.easyDebugAccess = easyDebugAccess
|
||||||
|
self.call = call
|
||||||
|
|
||||||
|
self.containerNode = ASDisplayNode()
|
||||||
|
if self.shouldStayHiddenUntilConnection {
|
||||||
|
self.containerNode.alpha = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.imageNode = TransformImageNode()
|
||||||
|
self.imageNode.contentAnimations = [.subsequentUpdates]
|
||||||
|
self.dimNode = ASDisplayNode()
|
||||||
|
self.dimNode.isUserInteractionEnabled = false
|
||||||
|
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
|
||||||
|
|
||||||
|
self.backButtonArrowNode = ASImageNode()
|
||||||
|
self.backButtonArrowNode.displayWithoutProcessing = true
|
||||||
|
self.backButtonArrowNode.displaysAsynchronously = false
|
||||||
|
self.backButtonArrowNode.image = NavigationBarTheme.generateBackArrowImage(color: .white)
|
||||||
|
self.backButtonNode = HighlightableButtonNode()
|
||||||
|
|
||||||
|
self.statusNode = CallControllerStatusNode()
|
||||||
|
|
||||||
|
self.videoPausedNode = ImmediateTextNode()
|
||||||
|
self.videoPausedNode.alpha = 0.0
|
||||||
|
|
||||||
|
self.buttonsNode = LegacyCallControllerButtonsNode(strings: self.presentationData.strings)
|
||||||
|
self.keyButtonNode = HighlightableButtonNode()
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.setViewBlock({
|
||||||
|
return UITracingLayerView()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.containerNode.backgroundColor = .black
|
||||||
|
|
||||||
|
self.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)
|
||||||
|
self.backButtonNode.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if highlighted {
|
||||||
|
strongSelf.backButtonNode.layer.removeAnimation(forKey: "opacity")
|
||||||
|
strongSelf.backButtonArrowNode.layer.removeAnimation(forKey: "opacity")
|
||||||
|
strongSelf.backButtonNode.alpha = 0.4
|
||||||
|
strongSelf.backButtonArrowNode.alpha = 0.4
|
||||||
|
} else {
|
||||||
|
strongSelf.backButtonNode.alpha = 1.0
|
||||||
|
strongSelf.backButtonArrowNode.alpha = 1.0
|
||||||
|
strongSelf.backButtonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||||
|
strongSelf.backButtonArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.containerNode.addSubnode(self.imageNode)
|
||||||
|
self.containerNode.addSubnode(self.dimNode)
|
||||||
|
self.containerNode.addSubnode(self.statusNode)
|
||||||
|
self.containerNode.addSubnode(self.videoPausedNode)
|
||||||
|
self.containerNode.addSubnode(self.buttonsNode)
|
||||||
|
self.containerNode.addSubnode(self.keyButtonNode)
|
||||||
|
self.containerNode.addSubnode(self.backButtonArrowNode)
|
||||||
|
self.containerNode.addSubnode(self.backButtonNode)
|
||||||
|
|
||||||
|
self.buttonsNode.mute = { [weak self] in
|
||||||
|
self?.toggleMute?()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buttonsNode.speaker = { [weak self] in
|
||||||
|
self?.beginAudioOuputSelection?()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buttonsNode.end = { [weak self] in
|
||||||
|
self?.endCall?()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buttonsNode.accept = { [weak self] in
|
||||||
|
self?.acceptCall?()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buttonsNode.toggleVideo = { [weak self] in
|
||||||
|
self?.toggleVideo?()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buttonsNode.rotateCamera = { [weak self] in
|
||||||
|
self?.call.switchVideoCamera()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
|
self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||||
|
self.view.addGestureRecognizer(panRecognizer)
|
||||||
|
|
||||||
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||||
|
self.view.addGestureRecognizer(tapRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePeer(accountPeer: Peer, peer: Peer, hasOther: Bool) {
|
||||||
|
if !arePeersEqual(self.peer, peer) {
|
||||||
|
self.peer = peer
|
||||||
|
if let peerReference = PeerReference(peer), !peer.profileImageRepresentations.isEmpty {
|
||||||
|
let representations: [ImageRepresentationWithReference] = peer.profileImageRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: .avatar(peer: peerReference, resource: $0.resource)) })
|
||||||
|
self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.account, representations: representations, autoFetchFullSize: true))
|
||||||
|
self.dimNode.isHidden = false
|
||||||
|
} else {
|
||||||
|
self.imageNode.setSignal(callDefaultBackground())
|
||||||
|
self.dimNode.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.statusNode.title = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
|
||||||
|
if hasOther {
|
||||||
|
self.statusNode.subtitle = self.presentationData.strings.Call_AnsweringWithAccount(accountPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).0
|
||||||
|
|
||||||
|
if let callState = callState {
|
||||||
|
self.updateCallState(callState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.videoPausedNode.attributedText = NSAttributedString(string: self.presentationData.strings.Call_RemoteVideoPaused(peer.compactDisplayTitle).0, font: Font.regular(17.0), textColor: .white)
|
||||||
|
|
||||||
|
if let (layout, navigationBarHeight) = self.validLayout {
|
||||||
|
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) {
|
||||||
|
if self.audioOutputState?.0 != availableOutputs || self.audioOutputState?.1 != currentOutput {
|
||||||
|
self.audioOutputState = (availableOutputs, currentOutput)
|
||||||
|
self.updateButtonsMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCallState(_ callState: PresentationCallState) {
|
||||||
|
self.callState = callState
|
||||||
|
|
||||||
|
let statusValue: CallControllerStatusValue
|
||||||
|
var statusReception: Int32?
|
||||||
|
|
||||||
|
switch callState.videoState {
|
||||||
|
case .active:
|
||||||
|
if !self.incomingVideoViewRequested {
|
||||||
|
self.incomingVideoViewRequested = true
|
||||||
|
self.call.makeIncomingVideoView(completion: { [weak self] incomingVideoView in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let incomingVideoView = incomingVideoView {
|
||||||
|
strongSelf.setCurrentAudioOutput?(.speaker)
|
||||||
|
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView)
|
||||||
|
strongSelf.incomingVideoNode = incomingVideoNode
|
||||||
|
strongSelf.containerNode.insertSubnode(incomingVideoNode, aboveSubnode: strongSelf.dimNode)
|
||||||
|
strongSelf.statusNode.isHidden = true
|
||||||
|
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||||
|
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !self.outgoingVideoViewRequested {
|
||||||
|
self.outgoingVideoViewRequested = true
|
||||||
|
self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let outgoingVideoView = outgoingVideoView {
|
||||||
|
outgoingVideoView.backgroundColor = .black
|
||||||
|
outgoingVideoView.clipsToBounds = true
|
||||||
|
strongSelf.setCurrentAudioOutput?(.speaker)
|
||||||
|
let outgoingVideoNode = OutgoingVideoNode(videoView: outgoingVideoView, switchCamera: {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.call.switchVideoCamera()
|
||||||
|
})
|
||||||
|
strongSelf.outgoingVideoNode = outgoingVideoNode
|
||||||
|
if let incomingVideoNode = strongSelf.incomingVideoNode {
|
||||||
|
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: incomingVideoNode)
|
||||||
|
} else {
|
||||||
|
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: strongSelf.dimNode)
|
||||||
|
}
|
||||||
|
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||||
|
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case .activeOutgoing:
|
||||||
|
if !self.outgoingVideoViewRequested {
|
||||||
|
self.outgoingVideoViewRequested = true
|
||||||
|
self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let outgoingVideoView = outgoingVideoView {
|
||||||
|
outgoingVideoView.backgroundColor = .black
|
||||||
|
outgoingVideoView.clipsToBounds = true
|
||||||
|
outgoingVideoView.layer.cornerRadius = 16.0
|
||||||
|
strongSelf.setCurrentAudioOutput?(.speaker)
|
||||||
|
let outgoingVideoNode = OutgoingVideoNode(videoView: outgoingVideoView, switchCamera: {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.call.switchVideoCamera()
|
||||||
|
})
|
||||||
|
strongSelf.outgoingVideoNode = outgoingVideoNode
|
||||||
|
if let incomingVideoNode = strongSelf.incomingVideoNode {
|
||||||
|
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: incomingVideoNode)
|
||||||
|
} else {
|
||||||
|
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: strongSelf.dimNode)
|
||||||
|
}
|
||||||
|
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||||
|
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if let incomingVideoNode = self.incomingVideoNode {
|
||||||
|
let isActive: Bool
|
||||||
|
switch callState.remoteVideoState {
|
||||||
|
case .inactive:
|
||||||
|
isActive = false
|
||||||
|
case .active:
|
||||||
|
isActive = true
|
||||||
|
}
|
||||||
|
incomingVideoNode.updateIsBlurred(isBlurred: !isActive)
|
||||||
|
if isActive != self.videoPausedNode.alpha.isZero {
|
||||||
|
if isActive {
|
||||||
|
self.videoPausedNode.alpha = 0.0
|
||||||
|
self.videoPausedNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||||
|
} else {
|
||||||
|
self.videoPausedNode.alpha = 1.0
|
||||||
|
self.videoPausedNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch callState.state {
|
||||||
|
case .waiting, .connecting:
|
||||||
|
statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
|
||||||
|
case let .requesting(ringing):
|
||||||
|
if ringing {
|
||||||
|
statusValue = .text(self.presentationData.strings.Call_StatusRinging)
|
||||||
|
} else {
|
||||||
|
statusValue = .text(self.presentationData.strings.Call_StatusRequesting)
|
||||||
|
}
|
||||||
|
case .terminating:
|
||||||
|
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
||||||
|
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)
|
||||||
|
case .hungUp, .missed:
|
||||||
|
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
||||||
|
}
|
||||||
|
case .error:
|
||||||
|
statusValue = .text(self.presentationData.strings.Call_StatusFailed)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
||||||
|
}
|
||||||
|
case .ringing:
|
||||||
|
var text = self.presentationData.strings.Call_StatusIncoming
|
||||||
|
if !self.statusNode.subtitle.isEmpty {
|
||||||
|
text += "\n\(self.statusNode.subtitle)"
|
||||||
|
}
|
||||||
|
statusValue = .text(text)
|
||||||
|
case .active(let timestamp, let reception, let keyVisualHash), .reconnecting(let timestamp, let reception, let keyVisualHash):
|
||||||
|
let strings = self.presentationData.strings
|
||||||
|
var isReconnecting = false
|
||||||
|
if case .reconnecting = callState.state {
|
||||||
|
isReconnecting = true
|
||||||
|
}
|
||||||
|
statusValue = .timer({ value in
|
||||||
|
if isReconnecting {
|
||||||
|
return strings.Call_StatusConnecting
|
||||||
|
} else {
|
||||||
|
return strings.Call_StatusOngoing(value).0
|
||||||
|
}
|
||||||
|
}, timestamp)
|
||||||
|
if self.keyTextData?.0 != keyVisualHash {
|
||||||
|
let text = stringForEmojiHashOfData(keyVisualHash, 4)!
|
||||||
|
self.keyTextData = (keyVisualHash, text)
|
||||||
|
|
||||||
|
self.keyButtonNode.setAttributedTitle(NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: Font.regular(22.0), NSAttributedString.Key.kern: 2.5 as NSNumber]), for: [])
|
||||||
|
|
||||||
|
let keyTextSize = self.keyButtonNode.measure(CGSize(width: 200.0, height: 200.0))
|
||||||
|
self.keyButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
|
self.keyButtonNode.frame = CGRect(origin: self.keyButtonNode.frame.origin, size: keyTextSize)
|
||||||
|
|
||||||
|
if let (layout, navigationBarHeight) = self.validLayout {
|
||||||
|
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
self.containerNode.alpha = 1.0
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.statusNode.status = statusValue
|
||||||
|
self.statusNode.reception = statusReception
|
||||||
|
|
||||||
|
self.updateButtonsMode()
|
||||||
|
|
||||||
|
if case let .terminated(id, _, reportRating) = callState.state, let callId = id {
|
||||||
|
let presentRating = reportRating || self.forceReportRating
|
||||||
|
if presentRating {
|
||||||
|
self.presentCallRating?(callId)
|
||||||
|
}
|
||||||
|
self.callEnded?(presentRating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateButtonsMode() {
|
||||||
|
guard let callState = self.callState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch callState.state {
|
||||||
|
case .ringing:
|
||||||
|
self.buttonsNode.updateMode(.incoming)
|
||||||
|
default:
|
||||||
|
var mode: LegacyCallControllerButtonsSpeakerMode = .none
|
||||||
|
if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput {
|
||||||
|
switch currentOutput {
|
||||||
|
case .builtin:
|
||||||
|
mode = .builtin
|
||||||
|
case .speaker:
|
||||||
|
mode = .speaker
|
||||||
|
case .headphones:
|
||||||
|
mode = .headphones
|
||||||
|
case .port:
|
||||||
|
mode = .bluetooth
|
||||||
|
}
|
||||||
|
if availableOutputs.count <= 1 {
|
||||||
|
mode = .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mappedVideoState: LegacyCallControllerButtonsMode.VideoState
|
||||||
|
switch callState.videoState {
|
||||||
|
case .notAvailable:
|
||||||
|
mappedVideoState = .notAvailable
|
||||||
|
case .available:
|
||||||
|
mappedVideoState = .available(true)
|
||||||
|
case .active:
|
||||||
|
mappedVideoState = .active
|
||||||
|
case .activeOutgoing:
|
||||||
|
mappedVideoState = .active
|
||||||
|
}
|
||||||
|
self.buttonsNode.updateMode(.active(speakerMode: mode, videoState: mappedVideoState))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn() {
|
||||||
|
var bounds = self.bounds
|
||||||
|
bounds.origin = CGPoint()
|
||||||
|
self.bounds = bounds
|
||||||
|
self.layer.removeAnimation(forKey: "bounds")
|
||||||
|
self.statusBar.layer.removeAnimation(forKey: "opacity")
|
||||||
|
self.containerNode.layer.removeAnimation(forKey: "opacity")
|
||||||
|
self.containerNode.layer.removeAnimation(forKey: "scale")
|
||||||
|
self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
|
if !self.shouldStayHiddenUntilConnection {
|
||||||
|
self.containerNode.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3)
|
||||||
|
self.containerNode.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.containerNode.alpha > 0.0 {
|
||||||
|
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||||
|
self.containerNode.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.validLayout = (layout, navigationBarHeight)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
|
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
|
|
||||||
|
if let keyPreviewNode = self.keyPreviewNode {
|
||||||
|
transition.updateFrame(node: keyPreviewNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
|
keyPreviewNode.updateLayout(size: layout.size, transition: .immediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
|
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 640.0, height: 640.0).aspectFilled(layout.size), boundingSize: layout.size, intrinsicInsets: UIEdgeInsets())
|
||||||
|
let apply = self.imageNode.asyncLayout()(arguments)
|
||||||
|
apply()
|
||||||
|
|
||||||
|
let navigationOffset: CGFloat = max(20.0, layout.safeInsets.top)
|
||||||
|
|
||||||
|
let backSize = self.backButtonNode.measure(CGSize(width: 320.0, height: 100.0))
|
||||||
|
if let image = self.backButtonArrowNode.image {
|
||||||
|
transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: 10.0, y: navigationOffset + 11.0), size: image.size))
|
||||||
|
}
|
||||||
|
transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: 29.0, y: navigationOffset + 11.0), size: backSize))
|
||||||
|
|
||||||
|
var statusOffset: CGFloat
|
||||||
|
if layout.metrics.widthClass == .regular && layout.metrics.heightClass == .regular {
|
||||||
|
if layout.size.height.isEqual(to: 1366.0) {
|
||||||
|
statusOffset = 160.0
|
||||||
|
} else {
|
||||||
|
statusOffset = 120.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if layout.size.height.isEqual(to: 736.0) {
|
||||||
|
statusOffset = 80.0
|
||||||
|
} else if layout.size.width.isEqual(to: 320.0) {
|
||||||
|
statusOffset = 60.0
|
||||||
|
} else {
|
||||||
|
statusOffset = 64.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusOffset += layout.safeInsets.top
|
||||||
|
|
||||||
|
let buttonsHeight: CGFloat = 75.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)))
|
||||||
|
|
||||||
|
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(constrainedWidth: layout.size.width, transition: transition)
|
||||||
|
let buttonsOriginY: CGFloat = 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)))
|
||||||
|
|
||||||
|
var outgoingVideoTransition = transition
|
||||||
|
if let incomingVideoNode = self.incomingVideoNode {
|
||||||
|
if incomingVideoNode.frame.width.isZero, let outgoingVideoNode = self.outgoingVideoNode, !outgoingVideoNode.frame.width.isZero, !transition.isAnimated {
|
||||||
|
outgoingVideoTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||||
|
}
|
||||||
|
incomingVideoNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||||
|
incomingVideoNode.updateLayout(size: layout.size)
|
||||||
|
}
|
||||||
|
if let outgoingVideoNode = self.outgoingVideoNode {
|
||||||
|
if self.incomingVideoNode == nil {
|
||||||
|
outgoingVideoNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||||
|
outgoingVideoNode.updateLayout(size: layout.size, isExpanded: true, transition: transition)
|
||||||
|
} else {
|
||||||
|
let outgoingSize = layout.size.aspectFitted(CGSize(width: 200.0, height: 200.0))
|
||||||
|
let outgoingFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - outgoingSize.width, y: buttonsOriginY - 32.0 - outgoingSize.height), size: outgoingSize)
|
||||||
|
outgoingVideoTransition.updateFrame(node: outgoingVideoNode, frame: outgoingFrame)
|
||||||
|
outgoingVideoNode.updateLayout(size: outgoingFrame.size, isExpanded: false, transition: outgoingVideoTransition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyTextSize = self.keyButtonNode.frame.size
|
||||||
|
transition.updateFrame(node: self.keyButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - keyTextSize.width - 8.0, y: navigationOffset + 8.0), size: keyTextSize))
|
||||||
|
|
||||||
|
if let debugNode = self.debugNode {
|
||||||
|
transition.updateFrame(node: debugNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func keyPressed() {
|
||||||
|
if self.keyPreviewNode == nil, let keyText = self.keyTextData?.1, let peer = self.peer {
|
||||||
|
let keyPreviewNode = CallControllerKeyPreviewNode(keyText: keyText, infoText: self.presentationData.strings.Call_EmojiDescription(peer.compactDisplayTitle).0.replacingOccurrences(of: "%%", with: "%"), dismiss: { [weak self] in
|
||||||
|
if let _ = self?.keyPreviewNode {
|
||||||
|
self?.backPressed()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.containerNode.insertSubnode(keyPreviewNode, belowSubnode: self.statusNode)
|
||||||
|
self.keyPreviewNode = keyPreviewNode
|
||||||
|
|
||||||
|
if let (validLayout, _) = self.validLayout {
|
||||||
|
keyPreviewNode.updateLayout(size: validLayout.size, transition: .immediate)
|
||||||
|
|
||||||
|
self.keyButtonNode.isHidden = true
|
||||||
|
keyPreviewNode.animateIn(from: self.keyButtonNode.frame, fromNode: self.keyButtonNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func backPressed() {
|
||||||
|
if let keyPreviewNode = self.keyPreviewNode {
|
||||||
|
self.keyPreviewNode = nil
|
||||||
|
keyPreviewNode.animateOut(to: self.keyButtonNode.frame, toNode: self.keyButtonNode, completion: { [weak self, weak keyPreviewNode] in
|
||||||
|
self?.keyButtonNode.isHidden = false
|
||||||
|
keyPreviewNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.back?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var debugTapCounter: (Double, Int) = (0.0, 0)
|
||||||
|
|
||||||
|
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
if let _ = self.keyPreviewNode {
|
||||||
|
self.backPressed()
|
||||||
|
} else {
|
||||||
|
let point = recognizer.location(in: recognizer.view)
|
||||||
|
if self.statusNode.frame.contains(point) {
|
||||||
|
if self.easyDebugAccess {
|
||||||
|
self.presentDebugNode()
|
||||||
|
} else {
|
||||||
|
let timestamp = CACurrentMediaTime()
|
||||||
|
if self.debugTapCounter.0 < timestamp - 0.75 {
|
||||||
|
self.debugTapCounter.0 = timestamp
|
||||||
|
self.debugTapCounter.1 = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.debugTapCounter.0 >= timestamp - 0.75 {
|
||||||
|
self.debugTapCounter.0 = timestamp
|
||||||
|
self.debugTapCounter.1 += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.debugTapCounter.1 >= 10 {
|
||||||
|
self.debugTapCounter.1 = 0
|
||||||
|
|
||||||
|
self.presentDebugNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentDebugNode() {
|
||||||
|
guard self.debugNode == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.forceReportRating = true
|
||||||
|
|
||||||
|
let debugNode = CallDebugNode(signal: self.debugInfo)
|
||||||
|
debugNode.dismiss = { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.debugNode?.removeFromSupernode()
|
||||||
|
strongSelf.debugNode = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.addSubnode(debugNode)
|
||||||
|
self.debugNode = debugNode
|
||||||
|
|
||||||
|
if let (layout, navigationBarHeight) = self.validLayout {
|
||||||
|
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
switch recognizer.state {
|
||||||
|
case .changed:
|
||||||
|
let offset = recognizer.translation(in: self.view).y
|
||||||
|
var bounds = self.bounds
|
||||||
|
bounds.origin.y = -offset
|
||||||
|
self.bounds = bounds
|
||||||
|
case .ended:
|
||||||
|
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 .cancelled:
|
||||||
|
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)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -167,7 +167,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
public let internalId: CallSessionInternalId
|
public let internalId: CallSessionInternalId
|
||||||
public let peerId: PeerId
|
public let peerId: PeerId
|
||||||
public let isOutgoing: Bool
|
public let isOutgoing: Bool
|
||||||
private var isVideo: Bool
|
public var isVideo: Bool
|
||||||
public let peer: Peer?
|
public let peer: Peer?
|
||||||
|
|
||||||
private let serializedData: String?
|
private let serializedData: String?
|
||||||
|
@ -39,14 +39,25 @@ public func unarchiveAutomaticallyArchivedPeer(account: Account, peerId: PeerId)
|
|||||||
let _ = (account.postbox.transaction { transaction -> Void in
|
let _ = (account.postbox.transaction { transaction -> Void in
|
||||||
updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root)
|
updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root)
|
||||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||||
guard let currentData = current as? CachedUserData, let currentStatusSettings = currentData.peerStatusSettings else {
|
if let currentData = current as? CachedUserData, let currentStatusSettings = currentData.peerStatusSettings {
|
||||||
return current
|
|
||||||
}
|
|
||||||
var statusSettings = currentStatusSettings
|
var statusSettings = currentStatusSettings
|
||||||
statusSettings.flags.remove(.canBlock)
|
statusSettings.flags.remove(.canBlock)
|
||||||
statusSettings.flags.remove(.canReport)
|
statusSettings.flags.remove(.canReport)
|
||||||
statusSettings.flags.remove(.autoArchived)
|
statusSettings.flags.remove(.autoArchived)
|
||||||
return currentData.withUpdatedPeerStatusSettings(statusSettings)
|
return currentData.withUpdatedPeerStatusSettings(statusSettings)
|
||||||
|
} else if let currentData = current as? CachedGroupData, let currentStatusSettings = currentData.peerStatusSettings {
|
||||||
|
var statusSettings = currentStatusSettings
|
||||||
|
statusSettings.flags.remove(.canReport)
|
||||||
|
statusSettings.flags.remove(.autoArchived)
|
||||||
|
return currentData.withUpdatedPeerStatusSettings(statusSettings)
|
||||||
|
} else if let currentData = current as? CachedChannelData, let currentStatusSettings = currentData.peerStatusSettings {
|
||||||
|
var statusSettings = currentStatusSettings
|
||||||
|
statusSettings.flags.remove(.canReport)
|
||||||
|
statusSettings.flags.remove(.autoArchived)
|
||||||
|
return currentData.withUpdatedPeerStatusSettings(statusSettings)
|
||||||
|
}else {
|
||||||
|
return current
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|> deliverOnMainQueue).start()
|
|> deliverOnMainQueue).start()
|
||||||
|
22
submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.7 KiB |
22
submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 545 B |
After Width: | Height: | Size: 844 B |
22
submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 655 B |
After Width: | Height: | Size: 1.2 KiB |
22
submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.9 KiB |
@ -87,6 +87,9 @@ private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReport
|
|||||||
} else if let _ = state.renderedPeer?.chatMainPeer {
|
} else if let _ = state.renderedPeer?.chatMainPeer {
|
||||||
if let contactStatus = state.contactStatus, contactStatus.canReportIrrelevantLocation, let peerStatusSettings = contactStatus.peerStatusSettings, peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
|
if let contactStatus = state.contactStatus, contactStatus.canReportIrrelevantLocation, let peerStatusSettings = contactStatus.peerStatusSettings, peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
|
||||||
buttons.append(.reportIrrelevantGeoLocation)
|
buttons.append(.reportIrrelevantGeoLocation)
|
||||||
|
} else if let contactStatus = state.contactStatus, let peerStatusSettings = contactStatus.peerStatusSettings, peerStatusSettings.contains(.autoArchived) {
|
||||||
|
buttons.append(.reportUserSpam)
|
||||||
|
buttons.append(.unarchive)
|
||||||
} else {
|
} else {
|
||||||
buttons.append(.reportSpam)
|
buttons.append(.reportSpam)
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,9 @@
|
|||||||
#include "api/video_track_source_proxy.h"
|
#include "api/video_track_source_proxy.h"
|
||||||
#include "sdk/objc/api/RTCVideoRendererAdapter.h"
|
#include "sdk/objc/api/RTCVideoRendererAdapter.h"
|
||||||
#include "sdk/objc/native/api/video_frame.h"
|
#include "sdk/objc/native/api/video_frame.h"
|
||||||
|
#if defined(WEBRTC_IOS)
|
||||||
#include "sdk/objc/components/audio/RTCAudioSession.h"
|
#include "sdk/objc/components/audio/RTCAudioSession.h"
|
||||||
|
#endif
|
||||||
#include "api/media_types.h"
|
#include "api/media_types.h"
|
||||||
|
|
||||||
#import "VideoCameraCapturer.h"
|
#import "VideoCameraCapturer.h"
|
||||||
@ -47,6 +49,9 @@
|
|||||||
|
|
||||||
_videoCapturer = [[VideoCameraCapturer alloc] initWithSource:source isActiveUpdated:isActiveUpdated];
|
_videoCapturer = [[VideoCameraCapturer alloc] initWithSource:source isActiveUpdated:isActiveUpdated];
|
||||||
|
|
||||||
|
AVCaptureDevice *selectedCamera = nil;
|
||||||
|
|
||||||
|
#if TARGET_OS_IOS
|
||||||
AVCaptureDevice *frontCamera = nil;
|
AVCaptureDevice *frontCamera = nil;
|
||||||
AVCaptureDevice *backCamera = nil;
|
AVCaptureDevice *backCamera = nil;
|
||||||
for (AVCaptureDevice *device in [VideoCameraCapturer captureDevices]) {
|
for (AVCaptureDevice *device in [VideoCameraCapturer captureDevices]) {
|
||||||
@ -56,14 +61,15 @@
|
|||||||
backCamera = device;
|
backCamera = device;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AVCaptureDevice *selectedCamera = nil;
|
|
||||||
if (useFrontCamera && frontCamera != nil) {
|
if (useFrontCamera && frontCamera != nil) {
|
||||||
selectedCamera = frontCamera;
|
selectedCamera = frontCamera;
|
||||||
} else {
|
} else {
|
||||||
selectedCamera = backCamera;
|
selectedCamera = backCamera;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
selectedCamera = [VideoCameraCapturer captureDevices].firstObject;
|
||||||
|
#endif
|
||||||
|
// NSLog(@"%@", selectedCamera);
|
||||||
if (selectedCamera == nil) {
|
if (selectedCamera == nil) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
@ -74,7 +80,7 @@
|
|||||||
return width1 < width2 ? NSOrderedAscending : NSOrderedDescending;
|
return width1 < width2 ? NSOrderedAscending : NSOrderedDescending;
|
||||||
}];
|
}];
|
||||||
|
|
||||||
AVCaptureDeviceFormat *bestFormat = nil;
|
AVCaptureDeviceFormat *bestFormat = sortedFormats.firstObject;
|
||||||
for (AVCaptureDeviceFormat *format in sortedFormats) {
|
for (AVCaptureDeviceFormat *format in sortedFormats) {
|
||||||
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
|
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
|
||||||
if (dimensions.width >= 1000 || dimensions.height >= 1000) {
|
if (dimensions.width >= 1000 || dimensions.height >= 1000) {
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
#include "TgVoip.h"
|
#include "TgVoip.h"
|
||||||
#include "VideoCaptureInterfaceImpl.h"
|
#include "VideoCaptureInterfaceImpl.h"
|
||||||
|
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE || TARGET_OS_OSX
|
||||||
|
|
||||||
#include "CodecsApple.h"
|
#include "CodecsApple.h"
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
#include "VideoCaptureInterfaceImpl.h"
|
#include "VideoCaptureInterfaceImpl.h"
|
||||||
|
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE || TARGET_OS_OSX
|
||||||
|
|
||||||
#include "CodecsApple.h"
|
#include "CodecsApple.h"
|
||||||
|
|
||||||
|