mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Call UI
This commit is contained in:
parent
5bfe7750cd
commit
820b038bbc
@ -333,10 +333,10 @@ public final class ManagedAudioSession: NSObject {
|
||||
var headphonesAreActive = false
|
||||
loop: for currentOutput in audioSession.currentRoute.outputs {
|
||||
switch currentOutput.portType {
|
||||
case .headphones, .bluetoothA2DP, .bluetoothHFP:
|
||||
case .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
|
||||
headphonesAreActive = true
|
||||
hasHeadphones = true
|
||||
hasBluetoothHeadphones = [.bluetoothA2DP, .bluetoothHFP].contains(currentOutput.portType)
|
||||
hasBluetoothHeadphones = [.bluetoothA2DP, .bluetoothHFP, .bluetoothLE].contains(currentOutput.portType)
|
||||
activeOutput = .headphones
|
||||
break loop
|
||||
default:
|
||||
@ -730,7 +730,7 @@ public final class ManagedAudioSession: NSObject {
|
||||
let route = AVAudioSession.sharedInstance().currentRoute
|
||||
//managedAudioSessionLog("\(route)")
|
||||
for desc in route.outputs {
|
||||
if desc.portType == .headphones || desc.portType == .bluetoothA2DP || desc.portType == .bluetoothHFP {
|
||||
if desc.portType == .headphones || desc.portType == .bluetoothA2DP || desc.portType == .bluetoothHFP || desc.portType == .bluetoothLE {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -977,7 +977,7 @@ public final class ManagedAudioSession: NSObject {
|
||||
} else {
|
||||
loop: for route in routes {
|
||||
switch route.portType {
|
||||
case .headphones, .bluetoothA2DP, .bluetoothHFP:
|
||||
case .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
|
||||
let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
|
||||
alreadySet = true
|
||||
break loop
|
||||
|
@ -50,6 +50,9 @@ public final class CallController: ViewController {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private let isDataReady = Promise<Bool>(false)
|
||||
private let isContentsReady = Promise<Bool>(false)
|
||||
|
||||
private let sharedContext: SharedAccountContext
|
||||
private let account: Account
|
||||
public let call: PresentationCall
|
||||
@ -85,6 +88,14 @@ public final class CallController: ViewController {
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self._ready.set(combineLatest(queue: .mainQueue(), self.isDataReady.get(), self.isContentsReady.get())
|
||||
|> map { a, b -> Bool in
|
||||
return a && b
|
||||
}
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> timeout(2.0, queue: .mainQueue(), alternate: .single(true)))
|
||||
|
||||
self.isOpaqueWhenInOverlay = true
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
@ -140,6 +151,7 @@ public final class CallController: ViewController {
|
||||
if self.sharedContext.immediateExperimentalUISettings.callUIV2 {
|
||||
let displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), easyDebugAccess: self.easyDebugAccess, call: self.call)
|
||||
self.displayNode = displayNode
|
||||
self.isContentsReady.set(displayNode.isReady.get())
|
||||
|
||||
displayNode.restoreUIForPictureInPicture = { [weak self] completion in
|
||||
guard let self, let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture else {
|
||||
@ -150,6 +162,7 @@ public final class CallController: ViewController {
|
||||
}
|
||||
} else {
|
||||
self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
|
||||
self.isContentsReady.set(.single(true))
|
||||
}
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
@ -320,7 +333,7 @@ public final class CallController: ViewController {
|
||||
if let accountPeer = accountView.peers[accountView.peerId], let peer = view.peers[view.peerId] {
|
||||
strongSelf.peer = peer
|
||||
strongSelf.controllerNode.updatePeer(accountPeer: accountPeer, peer: peer, hasOther: activeAccountsWithInfo.accounts.count > 1)
|
||||
strongSelf._ready.set(.single(true))
|
||||
strongSelf.isDataReady.set(.single(true))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -30,6 +30,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
private let callScreen: PrivateCallScreen
|
||||
private var callScreenState: PrivateCallScreen.State?
|
||||
|
||||
let isReady = Promise<Bool>()
|
||||
private var didInitializeIsReady: Bool = false
|
||||
|
||||
private var callStartTimestamp: Double?
|
||||
|
||||
private var callState: PresentationCallState?
|
||||
@ -307,18 +310,17 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
let mappedLifecycleState: PrivateCallScreen.State.LifecycleState
|
||||
switch callState.state {
|
||||
case .waiting:
|
||||
mappedLifecycleState = .connecting
|
||||
mappedLifecycleState = .requesting
|
||||
case .ringing:
|
||||
mappedLifecycleState = .ringing
|
||||
case let .requesting(isRinging):
|
||||
if isRinging {
|
||||
mappedLifecycleState = .ringing
|
||||
} else {
|
||||
mappedLifecycleState = .connecting
|
||||
mappedLifecycleState = .requesting
|
||||
}
|
||||
case let .connecting(keyData):
|
||||
let _ = keyData
|
||||
mappedLifecycleState = .exchangingKeys
|
||||
case .connecting:
|
||||
mappedLifecycleState = .connecting
|
||||
case let .active(startTime, signalQuality, keyData):
|
||||
self.callStartTimestamp = startTime
|
||||
|
||||
@ -332,20 +334,47 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
emojiKey: self.resolvedEmojiKey(data: keyData)
|
||||
))
|
||||
case let .reconnecting(startTime, _, keyData):
|
||||
let _ = keyData
|
||||
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
|
||||
startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
|
||||
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
|
||||
emojiKey: self.resolvedEmojiKey(data: keyData)
|
||||
))
|
||||
case .terminating, .terminated:
|
||||
if self.callStartTimestamp != nil {
|
||||
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
|
||||
startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
|
||||
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 0.0),
|
||||
emojiKey: self.resolvedEmojiKey(data: keyData)
|
||||
))
|
||||
} else {
|
||||
mappedLifecycleState = .connecting
|
||||
}
|
||||
case .terminating(let reason), .terminated(_, let reason, _):
|
||||
let duration: Double
|
||||
if let callStartTimestamp = self.callStartTimestamp {
|
||||
duration = CFAbsoluteTimeGetCurrent() - callStartTimestamp
|
||||
} else {
|
||||
duration = 0.0
|
||||
}
|
||||
mappedLifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: duration))
|
||||
|
||||
let mappedReason: PrivateCallScreen.State.TerminatedState.Reason
|
||||
if let reason {
|
||||
switch reason {
|
||||
case let .ended(type):
|
||||
switch type {
|
||||
case .missed:
|
||||
mappedReason = .missed
|
||||
case .busy:
|
||||
mappedReason = .busy
|
||||
case .hungUp:
|
||||
if self.callStartTimestamp != nil {
|
||||
mappedReason = .hangUp
|
||||
} else {
|
||||
mappedReason = .declined
|
||||
}
|
||||
}
|
||||
case .error:
|
||||
mappedReason = .failed
|
||||
}
|
||||
} else {
|
||||
mappedReason = .hangUp
|
||||
}
|
||||
|
||||
mappedLifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: duration, reason: mappedReason))
|
||||
}
|
||||
|
||||
switch callState.state {
|
||||
@ -404,6 +433,21 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
if case let .terminated(_, _, reportRating) = callState.state {
|
||||
self.callEnded?(reportRating)
|
||||
}
|
||||
|
||||
if !self.didInitializeIsReady {
|
||||
self.didInitializeIsReady = true
|
||||
|
||||
if let localVideo = self.localVideo {
|
||||
self.isReady.set(Signal { subscriber in
|
||||
return localVideo.addOnUpdated {
|
||||
subscriber.putNext(true)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.isReady.set(.single(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePeer(accountPeer: Peer, peer: Peer, hasOther: Bool) {
|
||||
|
@ -10,12 +10,8 @@ import AccountContext
|
||||
import TelegramAudio
|
||||
import TelegramVoip
|
||||
|
||||
private let sharedProviderDelegate: AnyObject? = {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
return CallKitProviderDelegate()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
private let sharedProviderDelegate: CallKitProviderDelegate? = {
|
||||
return CallKitProviderDelegate()
|
||||
}()
|
||||
|
||||
public final class CallKitIntegration {
|
||||
@ -53,69 +49,50 @@ public final class CallKitIntegration {
|
||||
setCallMuted: @escaping (UUID, Bool) -> Void,
|
||||
audioSessionActivationChanged: @escaping (Bool) -> Void
|
||||
) {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.setup(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, setCallMuted: setCallMuted, audioSessionActivationChanged: audioSessionActivationChanged, hasActiveCallsValue: hasActiveCallsValue)
|
||||
}
|
||||
sharedProviderDelegate?.setup(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, setCallMuted: setCallMuted, audioSessionActivationChanged: audioSessionActivationChanged, hasActiveCallsValue: hasActiveCallsValue)
|
||||
}
|
||||
|
||||
private init?() {
|
||||
if !CallKitIntegration.isAvailable {
|
||||
return nil
|
||||
}
|
||||
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func startCall(context: AccountContext, peerId: EnginePeer.Id, phoneNumber: String?, localContactId: String?, isVideo: Bool, displayTitle: String) {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle)
|
||||
self.donateIntent(peerId: peerId, displayTitle: displayTitle, localContactId: localContactId)
|
||||
}
|
||||
sharedProviderDelegate?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle)
|
||||
self.donateIntent(peerId: peerId, displayTitle: displayTitle, localContactId: localContactId)
|
||||
}
|
||||
|
||||
func answerCall(uuid: UUID) {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.answerCall(uuid: uuid)
|
||||
}
|
||||
sharedProviderDelegate?.answerCall(uuid: uuid)
|
||||
}
|
||||
|
||||
public func dropCall(uuid: UUID) {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.dropCall(uuid: uuid)
|
||||
}
|
||||
sharedProviderDelegate?.dropCall(uuid: uuid)
|
||||
}
|
||||
|
||||
public func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, phoneNumber: String?, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion)
|
||||
}
|
||||
sharedProviderDelegate?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion)
|
||||
}
|
||||
|
||||
func reportOutgoingCallConnected(uuid: UUID, at date: Date) {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.reportOutgoingCallConnected(uuid: uuid, at: date)
|
||||
}
|
||||
sharedProviderDelegate?.reportOutgoingCallConnected(uuid: uuid, at: date)
|
||||
}
|
||||
|
||||
private func donateIntent(peerId: EnginePeer.Id, displayTitle: String, localContactId: String?) {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
let handle = INPersonHandle(value: "tg\(peerId.id._internalGetInt64Value())", type: .unknown)
|
||||
let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: localContactId, customIdentifier: "tg\(peerId.id._internalGetInt64Value())")
|
||||
let handle = INPersonHandle(value: "tg\(peerId.id._internalGetInt64Value())", type: .unknown)
|
||||
let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: localContactId, customIdentifier: "tg\(peerId.id._internalGetInt64Value())")
|
||||
|
||||
let intent = INStartAudioCallIntent(destinationType: .normal, contacts: [contact])
|
||||
|
||||
let intent = INStartAudioCallIntent(destinationType: .normal, contacts: [contact])
|
||||
|
||||
let interaction = INInteraction(intent: intent, response: nil)
|
||||
interaction.direction = .outgoing
|
||||
interaction.donate { _ in
|
||||
}
|
||||
let interaction = INInteraction(intent: intent, response: nil)
|
||||
interaction.direction = .outgoing
|
||||
interaction.donate { _ in
|
||||
}
|
||||
}
|
||||
|
||||
public func applyVoiceChatOutputMode(outputMode: AudioSessionOutputMode) {
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.applyVoiceChatOutputMode(outputMode: outputMode)
|
||||
sharedProviderDelegate?.applyVoiceChatOutputMode(outputMode: outputMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,17 +55,20 @@ public final class AccountStateManager {
|
||||
public let callAccessHash: Int64
|
||||
public let timestamp: Int32
|
||||
public let peer: EnginePeer
|
||||
public let isVideo: Bool
|
||||
|
||||
init(
|
||||
callId: Int64,
|
||||
callAccessHash: Int64,
|
||||
timestamp: Int32,
|
||||
peer: EnginePeer
|
||||
peer: EnginePeer,
|
||||
isVideo: Bool
|
||||
) {
|
||||
self.callId = callId
|
||||
self.callAccessHash = callAccessHash
|
||||
self.timestamp = timestamp
|
||||
self.peer = peer
|
||||
self.isVideo = isVideo
|
||||
}
|
||||
}
|
||||
|
||||
@ -1821,7 +1824,7 @@ public final class AccountStateManager {
|
||||
switch update {
|
||||
case let .updatePhoneCall(phoneCall):
|
||||
switch phoneCall {
|
||||
case let .phoneCallRequested(_, id, accessHash, date, adminId, _, _, _):
|
||||
case let .phoneCallRequested(flags, id, accessHash, date, adminId, _, _, _):
|
||||
guard let peer = peers.first(where: { $0.id == PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)) }) else {
|
||||
return nil
|
||||
}
|
||||
@ -1829,7 +1832,8 @@ public final class AccountStateManager {
|
||||
callId: id,
|
||||
callAccessHash: accessHash,
|
||||
timestamp: date,
|
||||
peer: EnginePeer(peer)
|
||||
peer: EnginePeer(peer),
|
||||
isVideo: (flags & (1 << 6)) != 0
|
||||
)
|
||||
default:
|
||||
break
|
||||
|
@ -656,12 +656,24 @@ private final class CallSessionManagerContext {
|
||||
|
||||
if let (id, accessHash, reason) = dropData {
|
||||
self.contextIdByStableId.removeValue(forKey: id)
|
||||
let mappedReason: CallSessionTerminationReason = .ended(.hungUp)
|
||||
let mappedReason: CallSessionTerminationReason
|
||||
switch reason {
|
||||
case .abort:
|
||||
mappedReason = .ended(.hungUp)
|
||||
case .busy:
|
||||
mappedReason = .ended(.busy)
|
||||
case .disconnect:
|
||||
mappedReason = .error(.disconnected)
|
||||
case .hangUp:
|
||||
mappedReason = .ended(.hungUp)
|
||||
case .missed:
|
||||
mappedReason = .ended(.missed)
|
||||
}
|
||||
context.state = .dropping(reason: mappedReason, disposable: (dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, isVideo: isVideo, reason: reason)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in
|
||||
if let strongSelf = self {
|
||||
if let context = strongSelf.contexts[internalId] {
|
||||
context.state = .terminated(id: id, accessHash: accessHash, reason: .ended(.hungUp), reportRating: reportRating, sendDebugLogs: sendDebugLogs)
|
||||
context.state = .terminated(id: id, accessHash: accessHash, reason: mappedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs)
|
||||
/*if sendDebugLogs {
|
||||
let network = strongSelf.network
|
||||
let _ = (debugLog
|
||||
|
@ -38,10 +38,12 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
}
|
||||
|
||||
let content: Content
|
||||
let isEnabled: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(content: Content, action: @escaping () -> Void) {
|
||||
init(content: Content, isEnabled: Bool, action: @escaping () -> Void) {
|
||||
self.content = content
|
||||
self.isEnabled = isEnabled
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
@ -260,7 +262,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
transition.setAlpha(view: buttonView, alpha: displayClose ? 0.0 : 1.0)
|
||||
|
||||
buttonTransition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize)))
|
||||
buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: buttonTransition)
|
||||
buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, isEnabled: button.isEnabled, title: title, transition: buttonTransition)
|
||||
buttonX += buttonSize + buttonSpacing
|
||||
}
|
||||
|
||||
|
@ -9,12 +9,14 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
|
||||
var image: UIImage?
|
||||
var isSelected: Bool
|
||||
var isDestructive: Bool
|
||||
var isEnabled: Bool
|
||||
|
||||
init(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool) {
|
||||
init(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool, isEnabled: Bool) {
|
||||
self.size = size
|
||||
self.image = image
|
||||
self.isSelected = isSelected
|
||||
self.isDestructive = isDestructive
|
||||
self.isEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,13 +95,15 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
|
||||
self.action?()
|
||||
}
|
||||
|
||||
func update(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool, title: String, transition: Transition) {
|
||||
let contentParams = ContentParams(size: size, image: image, isSelected: isSelected, isDestructive: isDestructive)
|
||||
func update(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool, isEnabled: Bool, title: String, transition: Transition) {
|
||||
let contentParams = ContentParams(size: size, image: image, isSelected: isSelected, isDestructive: isDestructive, isEnabled: isEnabled)
|
||||
if self.contentParams != contentParams {
|
||||
self.contentParams = contentParams
|
||||
self.updateContent(contentParams: contentParams, transition: transition)
|
||||
}
|
||||
|
||||
self.isUserInteractionEnabled = isEnabled
|
||||
|
||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let textSize = self.textView.update(string: title, fontSize: 13.0, fontWeight: 0.0, color: .white, constrainedWidth: 100.0, transition: .immediate)
|
||||
@ -128,7 +132,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
|
||||
|
||||
context.clip(to: imageFrame, mask: cgImage)
|
||||
context.setBlendMode(contentParams.isSelected ? .copy : .normal)
|
||||
context.setFillColor(contentParams.isSelected ? UIColor.clear.cgColor : UIColor(white: 1.0, alpha: 1.0).cgColor)
|
||||
context.setFillColor(contentParams.isSelected ? UIColor(white: 1.0, alpha: contentParams.isEnabled ? 0.0 : 0.5).cgColor : UIColor(white: 1.0, alpha: contentParams.isEnabled ? 1.0 : 0.5).cgColor)
|
||||
context.fill(imageFrame)
|
||||
|
||||
context.resetClip()
|
||||
@ -136,12 +140,8 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
|
||||
}
|
||||
})
|
||||
|
||||
if !transition.animation.isImmediate, let currentContentViewIsSelected = self.currentContentViewIsSelected, currentContentViewIsSelected != contentParams.isSelected, let previousImage = self.contentView.image, let image {
|
||||
if !transition.animation.isImmediate, let currentContentViewIsSelected = self.currentContentViewIsSelected, currentContentViewIsSelected != contentParams.isSelected, let previousImage = self.contentView.image {
|
||||
self.contentView.layer.mask = nil
|
||||
let _ = previousImage
|
||||
let _ = image
|
||||
let _ = currentContentViewIsSelected
|
||||
|
||||
let previousContentView = UIImageView(image: previousImage)
|
||||
previousContentView.frame = self.contentView.frame
|
||||
self.addSubview(previousContentView)
|
||||
|
@ -166,7 +166,8 @@ final class StatusView: UIView {
|
||||
enum WaitingState {
|
||||
case requesting
|
||||
case ringing
|
||||
case generatingKeys
|
||||
case connecting
|
||||
case reconnecting
|
||||
}
|
||||
|
||||
struct ActiveState: Equatable {
|
||||
@ -299,8 +300,10 @@ final class StatusView: UIView {
|
||||
textString = "Requesting"
|
||||
case .ringing:
|
||||
textString = "Ringing"
|
||||
case .generatingKeys:
|
||||
textString = "Exchanging encryption keys"
|
||||
case .connecting:
|
||||
textString = "Connecting"
|
||||
case .reconnecting:
|
||||
textString = "Reconnecting"
|
||||
}
|
||||
case let .active(activeState):
|
||||
monospacedDigits = true
|
||||
@ -310,7 +313,11 @@ final class StatusView: UIView {
|
||||
textString = stringForDuration(Int(duration))
|
||||
signalStrength = activeState.signalStrength
|
||||
case let .terminated(terminatedState):
|
||||
textString = stringForDuration(Int(terminatedState.duration))
|
||||
if Int(terminatedState.duration) == 0 {
|
||||
textString = " "
|
||||
} else {
|
||||
textString = stringForDuration(Int(terminatedState.duration))
|
||||
}
|
||||
}
|
||||
|
||||
var contentSize = CGSize()
|
||||
|
@ -31,17 +31,28 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
}
|
||||
|
||||
public struct TerminatedState: Equatable {
|
||||
public var duration: Double
|
||||
public enum Reason {
|
||||
case missed
|
||||
case hangUp
|
||||
case failed
|
||||
case busy
|
||||
case declined
|
||||
}
|
||||
|
||||
public init(duration: Double) {
|
||||
public var duration: Double
|
||||
public var reason: Reason
|
||||
|
||||
public init(duration: Double, reason: Reason) {
|
||||
self.duration = duration
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
|
||||
public enum LifecycleState: Equatable {
|
||||
case connecting
|
||||
case requesting
|
||||
case ringing
|
||||
case exchangingKeys
|
||||
case connecting
|
||||
case reconnecting
|
||||
case active(ActiveState)
|
||||
case terminated(TerminatedState)
|
||||
}
|
||||
@ -177,6 +188,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
private var swapLocalAndRemoteVideo: Bool = false
|
||||
private var isPictureInPictureActive: Bool = false
|
||||
|
||||
private var hideEmojiTooltipTimer: Foundation.Timer?
|
||||
private var hideControlsTimer: Foundation.Timer?
|
||||
|
||||
private var processedInitialAudioLevelBump: Bool = false
|
||||
private var audioLevelBump: Float = 0.0
|
||||
|
||||
@ -500,8 +514,20 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
|
||||
if let previousParams = self.params, case .active = params.state.lifecycleState {
|
||||
switch previousParams.state.lifecycleState {
|
||||
case .connecting, .exchangingKeys, .ringing:
|
||||
self.displayEmojiTooltip = true
|
||||
case .requesting, .ringing, .connecting, .reconnecting:
|
||||
if self.hideEmojiTooltipTimer == nil {
|
||||
self.displayEmojiTooltip = true
|
||||
|
||||
self.hideEmojiTooltipTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.displayEmojiTooltip {
|
||||
self.displayEmojiTooltip = false
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
})
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -559,6 +585,18 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
}
|
||||
let havePrimaryVideo = !activeVideoSources.isEmpty
|
||||
|
||||
if havePrimaryVideo && self.hideControlsTimer == nil {
|
||||
self.hideControlsTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.areControlsHidden {
|
||||
self.areControlsHidden = true
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
if havePrimaryVideo, let pipVideoCallViewController = self.pipVideoCallViewController as? AVPictureInPictureVideoCallViewController {
|
||||
if self.pipController == nil {
|
||||
@ -607,11 +645,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
|
||||
let backgroundStateIndex: Int
|
||||
switch params.state.lifecycleState {
|
||||
case .connecting:
|
||||
backgroundStateIndex = 0
|
||||
case .ringing:
|
||||
backgroundStateIndex = 0
|
||||
case .exchangingKeys:
|
||||
case .requesting, .ringing, .connecting, .reconnecting:
|
||||
backgroundStateIndex = 0
|
||||
case let .active(activeState):
|
||||
if activeState.signalInfo.quality <= 0.2 {
|
||||
@ -626,20 +660,36 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
|
||||
transition.setFrame(view: self.buttonGroupView, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||
|
||||
var isVideoButtonEnabled = false
|
||||
switch params.state.lifecycleState {
|
||||
case .active, .reconnecting:
|
||||
isVideoButtonEnabled = true
|
||||
default:
|
||||
isVideoButtonEnabled = false
|
||||
}
|
||||
|
||||
var isTerminated = false
|
||||
switch params.state.lifecycleState {
|
||||
case .terminated:
|
||||
isTerminated = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var buttons: [ButtonGroupView.Button] = [
|
||||
ButtonGroupView.Button(content: .video(isActive: params.state.localVideo != nil), action: { [weak self] in
|
||||
ButtonGroupView.Button(content: .video(isActive: params.state.localVideo != nil), isEnabled: isVideoButtonEnabled && !isTerminated, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.videoAction?()
|
||||
}),
|
||||
ButtonGroupView.Button(content: .microphone(isMuted: params.state.isLocalAudioMuted), action: { [weak self] in
|
||||
ButtonGroupView.Button(content: .microphone(isMuted: params.state.isLocalAudioMuted), isEnabled: !isTerminated, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.microhoneMuteAction?()
|
||||
}),
|
||||
ButtonGroupView.Button(content: .end, action: { [weak self] in
|
||||
ButtonGroupView.Button(content: .end, isEnabled: !isTerminated, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -647,14 +697,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
})
|
||||
]
|
||||
if self.activeLocalVideoSource != nil {
|
||||
buttons.insert(ButtonGroupView.Button(content: .flipCamera, action: { [weak self] in
|
||||
buttons.insert(ButtonGroupView.Button(content: .flipCamera, isEnabled: !isTerminated, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.flipCameraAction?()
|
||||
}), at: 0)
|
||||
} else {
|
||||
buttons.insert(ButtonGroupView.Button(content: .speaker(isActive: params.state.audioOutput != .internalSpeaker), action: { [weak self] in
|
||||
buttons.insert(ButtonGroupView.Button(content: .speaker(isActive: params.state.audioOutput != .internalSpeaker), isEnabled: !isTerminated, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -663,23 +713,27 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
}
|
||||
|
||||
var notices: [ButtonGroupView.Notice] = []
|
||||
if params.state.isLocalAudioMuted {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "Your microphone is turned off"))
|
||||
}
|
||||
if params.state.isRemoteAudioMuted {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "\(params.state.shortName)'s microphone is turned off"))
|
||||
}
|
||||
if params.state.remoteVideo != nil && params.state.localVideo == nil {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "Your camera is turned off"))
|
||||
}
|
||||
if params.state.isRemoteBatteryLow {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), text: "\(params.state.shortName)'s battery is low"))
|
||||
if !isTerminated {
|
||||
if params.state.isLocalAudioMuted {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "Your microphone is turned off"))
|
||||
}
|
||||
if params.state.isRemoteAudioMuted {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "\(params.state.shortName)'s microphone is turned off"))
|
||||
}
|
||||
if params.state.remoteVideo != nil && params.state.localVideo == nil {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "Your camera is turned off"))
|
||||
}
|
||||
if params.state.isRemoteBatteryLow {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), text: "\(params.state.shortName)'s battery is low"))
|
||||
}
|
||||
}
|
||||
|
||||
var displayClose = false
|
||||
/*var displayClose = false
|
||||
if case .terminated = params.state.lifecycleState {
|
||||
displayClose = true
|
||||
}
|
||||
}*/
|
||||
let displayClose = false
|
||||
|
||||
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, buttons: buttons, notices: notices, transition: transition)
|
||||
|
||||
var expandedEmojiKeyRect: CGRect?
|
||||
@ -1105,9 +1159,21 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
|
||||
let titleString: String
|
||||
switch params.state.lifecycleState {
|
||||
case .terminated:
|
||||
case let .terminated(terminatedState):
|
||||
self.titleView.contentMode = .center
|
||||
titleString = "Call Ended"
|
||||
|
||||
switch terminatedState.reason {
|
||||
case .busy:
|
||||
titleString = "Line Busy"
|
||||
case .declined:
|
||||
titleString = "Call Declined"
|
||||
case .failed:
|
||||
titleString = "Call Failed"
|
||||
case .hangUp:
|
||||
titleString = "Call Ended"
|
||||
case .missed:
|
||||
titleString = "Call Missed"
|
||||
}
|
||||
genericAlphaTransition.setScale(layer: self.blobLayer, scale: 0.3)
|
||||
genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: 0.0)
|
||||
self.canAnimateAudioLevel = false
|
||||
@ -1133,12 +1199,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
|
||||
let statusState: StatusView.State
|
||||
switch params.state.lifecycleState {
|
||||
case .connecting:
|
||||
case .requesting:
|
||||
statusState = .waiting(.requesting)
|
||||
case .connecting:
|
||||
statusState = .waiting(.connecting)
|
||||
case .reconnecting:
|
||||
statusState = .waiting(.reconnecting)
|
||||
case .ringing:
|
||||
statusState = .waiting(.ringing)
|
||||
case .exchangingKeys:
|
||||
statusState = .waiting(.generatingKeys)
|
||||
case let .active(activeState):
|
||||
statusState = .active(StatusView.ActiveState(startTimestamp: activeState.startTime, signalStrength: activeState.signalInfo.quality))
|
||||
|
||||
|
@ -2030,7 +2030,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
stableId: callUpdate.callId,
|
||||
handle: "\(callUpdate.peer.id.id._internalGetInt64Value())",
|
||||
phoneNumber: phoneNumber.flatMap(formatPhoneNumber),
|
||||
isVideo: false,
|
||||
isVideo: callUpdate.isVideo,
|
||||
displayTitle: callUpdate.peer.debugDisplayTitle,
|
||||
completion: { error in
|
||||
if let error = error {
|
||||
|
Loading…
x
Reference in New Issue
Block a user