[WIP] Call UI

This commit is contained in:
Isaac 2023-12-18 02:06:39 +04:00
parent 5bfe7750cd
commit 820b038bbc
11 changed files with 240 additions and 113 deletions

View File

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

View File

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

View File

@ -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
if self.callStartTimestamp != nil {
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 0.0),
emojiKey: self.resolvedEmojiKey(data: keyData)
))
case .terminating, .terminated:
} 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) {

View File

@ -10,12 +10,8 @@ import AccountContext
import TelegramAudio
import TelegramVoip
private let sharedProviderDelegate: AnyObject? = {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
private let sharedProviderDelegate: CallKitProviderDelegate? = {
return CallKitProviderDelegate()
} else {
return nil
}
}()
public final class CallKitIntegration {
@ -53,55 +49,37 @@ 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)
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())")
@ -112,10 +90,9 @@ public final class CallKitIntegration {
interaction.donate { _ in
}
}
}
public func applyVoiceChatOutputMode(outputMode: AudioSessionOutputMode) {
(sharedProviderDelegate as? CallKitProviderDelegate)?.applyVoiceChatOutputMode(outputMode: outputMode)
sharedProviderDelegate?.applyVoiceChatOutputMode(outputMode: outputMode)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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,8 +313,12 @@ final class StatusView: UIView {
textString = stringForDuration(Int(duration))
signalStrength = activeState.signalStrength
case let .terminated(terminatedState):
if Int(terminatedState.duration) == 0 {
textString = " "
} else {
textString = stringForDuration(Int(terminatedState.duration))
}
}
var contentSize = CGSize()

View File

@ -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:
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,6 +713,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
}
var notices: [ButtonGroupView.Notice] = []
if !isTerminated {
if params.state.isLocalAudioMuted {
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "Your microphone is turned off"))
}
@ -675,11 +726,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
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
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))

View File

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