[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 var headphonesAreActive = false
loop: for currentOutput in audioSession.currentRoute.outputs { loop: for currentOutput in audioSession.currentRoute.outputs {
switch currentOutput.portType { switch currentOutput.portType {
case .headphones, .bluetoothA2DP, .bluetoothHFP: case .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
headphonesAreActive = true headphonesAreActive = true
hasHeadphones = true hasHeadphones = true
hasBluetoothHeadphones = [.bluetoothA2DP, .bluetoothHFP].contains(currentOutput.portType) hasBluetoothHeadphones = [.bluetoothA2DP, .bluetoothHFP, .bluetoothLE].contains(currentOutput.portType)
activeOutput = .headphones activeOutput = .headphones
break loop break loop
default: default:
@ -730,7 +730,7 @@ public final class ManagedAudioSession: NSObject {
let route = AVAudioSession.sharedInstance().currentRoute let route = AVAudioSession.sharedInstance().currentRoute
//managedAudioSessionLog("\(route)") //managedAudioSessionLog("\(route)")
for desc in route.outputs { 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 return true
} }
} }
@ -977,7 +977,7 @@ public final class ManagedAudioSession: NSObject {
} else { } else {
loop: for route in routes { loop: for route in routes {
switch route.portType { switch route.portType {
case .headphones, .bluetoothA2DP, .bluetoothHFP: case .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route) let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
alreadySet = true alreadySet = true
break loop break loop

View File

@ -50,6 +50,9 @@ public final class CallController: ViewController {
return self._ready return self._ready
} }
private let isDataReady = Promise<Bool>(false)
private let isContentsReady = Promise<Bool>(false)
private let sharedContext: SharedAccountContext private let sharedContext: SharedAccountContext
private let account: Account private let account: Account
public let call: PresentationCall public let call: PresentationCall
@ -85,6 +88,14 @@ public final class CallController: ViewController {
super.init(navigationBarPresentationData: nil) 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.isOpaqueWhenInOverlay = true
self.statusBar.statusBarStyle = .White self.statusBar.statusBarStyle = .White
@ -140,6 +151,7 @@ public final class CallController: ViewController {
if self.sharedContext.immediateExperimentalUISettings.callUIV2 { 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) 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.displayNode = displayNode
self.isContentsReady.set(displayNode.isReady.get())
displayNode.restoreUIForPictureInPicture = { [weak self] completion in displayNode.restoreUIForPictureInPicture = { [weak self] completion in
guard let self, let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture else { guard let self, let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture else {
@ -150,6 +162,7 @@ public final class CallController: ViewController {
} }
} else { } 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.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() self.displayNodeDidLoad()
@ -320,7 +333,7 @@ public final class CallController: ViewController {
if let accountPeer = accountView.peers[accountView.peerId], let peer = view.peers[view.peerId] { if let accountPeer = accountView.peers[accountView.peerId], let peer = view.peers[view.peerId] {
strongSelf.peer = peer strongSelf.peer = peer
strongSelf.controllerNode.updatePeer(accountPeer: accountPeer, peer: peer, hasOther: activeAccountsWithInfo.accounts.count > 1) 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 let callScreen: PrivateCallScreen
private var callScreenState: PrivateCallScreen.State? private var callScreenState: PrivateCallScreen.State?
let isReady = Promise<Bool>()
private var didInitializeIsReady: Bool = false
private var callStartTimestamp: Double? private var callStartTimestamp: Double?
private var callState: PresentationCallState? private var callState: PresentationCallState?
@ -307,18 +310,17 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
let mappedLifecycleState: PrivateCallScreen.State.LifecycleState let mappedLifecycleState: PrivateCallScreen.State.LifecycleState
switch callState.state { switch callState.state {
case .waiting: case .waiting:
mappedLifecycleState = .connecting mappedLifecycleState = .requesting
case .ringing: case .ringing:
mappedLifecycleState = .ringing mappedLifecycleState = .ringing
case let .requesting(isRinging): case let .requesting(isRinging):
if isRinging { if isRinging {
mappedLifecycleState = .ringing mappedLifecycleState = .ringing
} else { } else {
mappedLifecycleState = .connecting mappedLifecycleState = .requesting
} }
case let .connecting(keyData): case .connecting:
let _ = keyData mappedLifecycleState = .connecting
mappedLifecycleState = .exchangingKeys
case let .active(startTime, signalQuality, keyData): case let .active(startTime, signalQuality, keyData):
self.callStartTimestamp = startTime self.callStartTimestamp = startTime
@ -332,20 +334,47 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
emojiKey: self.resolvedEmojiKey(data: keyData) emojiKey: self.resolvedEmojiKey(data: keyData)
)) ))
case let .reconnecting(startTime, _, keyData): case let .reconnecting(startTime, _, keyData):
let _ = keyData if self.callStartTimestamp != nil {
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
startTime: startTime + kCFAbsoluteTimeIntervalSince1970, startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0), signalInfo: PrivateCallScreen.State.SignalInfo(quality: 0.0),
emojiKey: self.resolvedEmojiKey(data: keyData) emojiKey: self.resolvedEmojiKey(data: keyData)
)) ))
case .terminating, .terminated: } else {
mappedLifecycleState = .connecting
}
case .terminating(let reason), .terminated(_, let reason, _):
let duration: Double let duration: Double
if let callStartTimestamp = self.callStartTimestamp { if let callStartTimestamp = self.callStartTimestamp {
duration = CFAbsoluteTimeGetCurrent() - callStartTimestamp duration = CFAbsoluteTimeGetCurrent() - callStartTimestamp
} else { } else {
duration = 0.0 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 { switch callState.state {
@ -404,6 +433,21 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
if case let .terminated(_, _, reportRating) = callState.state { if case let .terminated(_, _, reportRating) = callState.state {
self.callEnded?(reportRating) 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) { func updatePeer(accountPeer: Peer, peer: Peer, hasOther: Bool) {

View File

@ -10,12 +10,8 @@ import AccountContext
import TelegramAudio import TelegramAudio
import TelegramVoip import TelegramVoip
private let sharedProviderDelegate: AnyObject? = { private let sharedProviderDelegate: CallKitProviderDelegate? = {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { return CallKitProviderDelegate()
return CallKitProviderDelegate()
} else {
return nil
}
}() }()
public final class CallKitIntegration { public final class CallKitIntegration {
@ -53,69 +49,50 @@ public final class CallKitIntegration {
setCallMuted: @escaping (UUID, Bool) -> Void, setCallMuted: @escaping (UUID, Bool) -> Void,
audioSessionActivationChanged: @escaping (Bool) -> Void audioSessionActivationChanged: @escaping (Bool) -> Void
) { ) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { sharedProviderDelegate?.setup(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, setCallMuted: setCallMuted, audioSessionActivationChanged: audioSessionActivationChanged, hasActiveCallsValue: hasActiveCallsValue)
(sharedProviderDelegate as? CallKitProviderDelegate)?.setup(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, setCallMuted: setCallMuted, audioSessionActivationChanged: audioSessionActivationChanged, hasActiveCallsValue: hasActiveCallsValue)
}
} }
private init?() { private init?() {
if !CallKitIntegration.isAvailable { if !CallKitIntegration.isAvailable {
return nil 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) { func startCall(context: AccountContext, peerId: EnginePeer.Id, phoneNumber: String?, localContactId: String?, isVideo: Bool, displayTitle: String) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { sharedProviderDelegate?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle)
(sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle) self.donateIntent(peerId: peerId, displayTitle: displayTitle, localContactId: localContactId)
self.donateIntent(peerId: peerId, displayTitle: displayTitle, localContactId: localContactId)
}
} }
func answerCall(uuid: UUID) { func answerCall(uuid: UUID) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { sharedProviderDelegate?.answerCall(uuid: uuid)
(sharedProviderDelegate as? CallKitProviderDelegate)?.answerCall(uuid: uuid)
}
} }
public func dropCall(uuid: UUID) { public func dropCall(uuid: UUID) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { sharedProviderDelegate?.dropCall(uuid: uuid)
(sharedProviderDelegate as? CallKitProviderDelegate)?.dropCall(uuid: uuid)
}
} }
public func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, phoneNumber: String?, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) { 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?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion)
(sharedProviderDelegate as? CallKitProviderDelegate)?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion)
}
} }
func reportOutgoingCallConnected(uuid: UUID, at date: Date) { func reportOutgoingCallConnected(uuid: UUID, at date: Date) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { sharedProviderDelegate?.reportOutgoingCallConnected(uuid: uuid, at: date)
(sharedProviderDelegate as? CallKitProviderDelegate)?.reportOutgoingCallConnected(uuid: uuid, at: date)
}
} }
private func donateIntent(peerId: EnginePeer.Id, displayTitle: String, localContactId: String?) { 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 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 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
let interaction = INInteraction(intent: intent, response: nil) interaction.donate { _ in
interaction.direction = .outgoing
interaction.donate { _ in
}
} }
} }
public func applyVoiceChatOutputMode(outputMode: AudioSessionOutputMode) { 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 callAccessHash: Int64
public let timestamp: Int32 public let timestamp: Int32
public let peer: EnginePeer public let peer: EnginePeer
public let isVideo: Bool
init( init(
callId: Int64, callId: Int64,
callAccessHash: Int64, callAccessHash: Int64,
timestamp: Int32, timestamp: Int32,
peer: EnginePeer peer: EnginePeer,
isVideo: Bool
) { ) {
self.callId = callId self.callId = callId
self.callAccessHash = callAccessHash self.callAccessHash = callAccessHash
self.timestamp = timestamp self.timestamp = timestamp
self.peer = peer self.peer = peer
self.isVideo = isVideo
} }
} }
@ -1821,7 +1824,7 @@ public final class AccountStateManager {
switch update { switch update {
case let .updatePhoneCall(phoneCall): case let .updatePhoneCall(phoneCall):
switch 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 { guard let peer = peers.first(where: { $0.id == PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)) }) else {
return nil return nil
} }
@ -1829,7 +1832,8 @@ public final class AccountStateManager {
callId: id, callId: id,
callAccessHash: accessHash, callAccessHash: accessHash,
timestamp: date, timestamp: date,
peer: EnginePeer(peer) peer: EnginePeer(peer),
isVideo: (flags & (1 << 6)) != 0
) )
default: default:
break break

View File

@ -656,12 +656,24 @@ private final class CallSessionManagerContext {
if let (id, accessHash, reason) = dropData { if let (id, accessHash, reason) = dropData {
self.contextIdByStableId.removeValue(forKey: id) 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) 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 |> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in
if let strongSelf = self { if let strongSelf = self {
if let context = strongSelf.contexts[internalId] { 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 { /*if sendDebugLogs {
let network = strongSelf.network let network = strongSelf.network
let _ = (debugLog let _ = (debugLog

View File

@ -38,10 +38,12 @@ final class ButtonGroupView: OverlayMaskContainerView {
} }
let content: Content let content: Content
let isEnabled: Bool
let action: () -> Void let action: () -> Void
init(content: Content, action: @escaping () -> Void) { init(content: Content, isEnabled: Bool, action: @escaping () -> Void) {
self.content = content self.content = content
self.isEnabled = isEnabled
self.action = action self.action = action
} }
} }
@ -260,7 +262,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
transition.setAlpha(view: buttonView, alpha: displayClose ? 0.0 : 1.0) transition.setAlpha(view: buttonView, alpha: displayClose ? 0.0 : 1.0)
buttonTransition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize))) 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 buttonX += buttonSize + buttonSpacing
} }

View File

@ -9,12 +9,14 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
var image: UIImage? var image: UIImage?
var isSelected: Bool var isSelected: Bool
var isDestructive: 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.size = size
self.image = image self.image = image
self.isSelected = isSelected self.isSelected = isSelected
self.isDestructive = isDestructive self.isDestructive = isDestructive
self.isEnabled = isEnabled
} }
} }
@ -93,13 +95,15 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
self.action?() self.action?()
} }
func update(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool, title: String, transition: Transition) { 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) let contentParams = ContentParams(size: size, image: image, isSelected: isSelected, isDestructive: isDestructive, isEnabled: isEnabled)
if self.contentParams != contentParams { if self.contentParams != contentParams {
self.contentParams = contentParams self.contentParams = contentParams
self.updateContent(contentParams: contentParams, transition: transition) self.updateContent(contentParams: contentParams, transition: transition)
} }
self.isUserInteractionEnabled = isEnabled
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size)) 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) 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.clip(to: imageFrame, mask: cgImage)
context.setBlendMode(contentParams.isSelected ? .copy : .normal) 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.fill(imageFrame)
context.resetClip() 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 self.contentView.layer.mask = nil
let _ = previousImage
let _ = image
let _ = currentContentViewIsSelected
let previousContentView = UIImageView(image: previousImage) let previousContentView = UIImageView(image: previousImage)
previousContentView.frame = self.contentView.frame previousContentView.frame = self.contentView.frame
self.addSubview(previousContentView) self.addSubview(previousContentView)

View File

@ -166,7 +166,8 @@ final class StatusView: UIView {
enum WaitingState { enum WaitingState {
case requesting case requesting
case ringing case ringing
case generatingKeys case connecting
case reconnecting
} }
struct ActiveState: Equatable { struct ActiveState: Equatable {
@ -299,8 +300,10 @@ final class StatusView: UIView {
textString = "Requesting" textString = "Requesting"
case .ringing: case .ringing:
textString = "Ringing" textString = "Ringing"
case .generatingKeys: case .connecting:
textString = "Exchanging encryption keys" textString = "Connecting"
case .reconnecting:
textString = "Reconnecting"
} }
case let .active(activeState): case let .active(activeState):
monospacedDigits = true monospacedDigits = true
@ -310,7 +313,11 @@ final class StatusView: UIView {
textString = stringForDuration(Int(duration)) textString = stringForDuration(Int(duration))
signalStrength = activeState.signalStrength signalStrength = activeState.signalStrength
case let .terminated(terminatedState): case let .terminated(terminatedState):
textString = stringForDuration(Int(terminatedState.duration)) if Int(terminatedState.duration) == 0 {
textString = " "
} else {
textString = stringForDuration(Int(terminatedState.duration))
}
} }
var contentSize = CGSize() var contentSize = CGSize()

View File

@ -31,17 +31,28 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
} }
public struct TerminatedState: Equatable { 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.duration = duration
self.reason = reason
} }
} }
public enum LifecycleState: Equatable { public enum LifecycleState: Equatable {
case connecting case requesting
case ringing case ringing
case exchangingKeys case connecting
case reconnecting
case active(ActiveState) case active(ActiveState)
case terminated(TerminatedState) case terminated(TerminatedState)
} }
@ -177,6 +188,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
private var swapLocalAndRemoteVideo: Bool = false private var swapLocalAndRemoteVideo: Bool = false
private var isPictureInPictureActive: Bool = false private var isPictureInPictureActive: Bool = false
private var hideEmojiTooltipTimer: Foundation.Timer?
private var hideControlsTimer: Foundation.Timer?
private var processedInitialAudioLevelBump: Bool = false private var processedInitialAudioLevelBump: Bool = false
private var audioLevelBump: Float = 0.0 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 { if let previousParams = self.params, case .active = params.state.lifecycleState {
switch previousParams.state.lifecycleState { switch previousParams.state.lifecycleState {
case .connecting, .exchangingKeys, .ringing: case .requesting, .ringing, .connecting, .reconnecting:
self.displayEmojiTooltip = true 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: default:
break break
} }
@ -559,6 +585,18 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
} }
let havePrimaryVideo = !activeVideoSources.isEmpty 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 #available(iOS 16.0, *) {
if havePrimaryVideo, let pipVideoCallViewController = self.pipVideoCallViewController as? AVPictureInPictureVideoCallViewController { if havePrimaryVideo, let pipVideoCallViewController = self.pipVideoCallViewController as? AVPictureInPictureVideoCallViewController {
if self.pipController == nil { if self.pipController == nil {
@ -607,11 +645,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
let backgroundStateIndex: Int let backgroundStateIndex: Int
switch params.state.lifecycleState { switch params.state.lifecycleState {
case .connecting: case .requesting, .ringing, .connecting, .reconnecting:
backgroundStateIndex = 0
case .ringing:
backgroundStateIndex = 0
case .exchangingKeys:
backgroundStateIndex = 0 backgroundStateIndex = 0
case let .active(activeState): case let .active(activeState):
if activeState.signalInfo.quality <= 0.2 { 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)) 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] = [ 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 { guard let self else {
return return
} }
self.videoAction?() 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 { guard let self else {
return return
} }
self.microhoneMuteAction?() self.microhoneMuteAction?()
}), }),
ButtonGroupView.Button(content: .end, action: { [weak self] in ButtonGroupView.Button(content: .end, isEnabled: !isTerminated, action: { [weak self] in
guard let self else { guard let self else {
return return
} }
@ -647,14 +697,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
}) })
] ]
if self.activeLocalVideoSource != nil { 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 { guard let self else {
return return
} }
self.flipCameraAction?() self.flipCameraAction?()
}), at: 0) }), at: 0)
} else { } 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 { guard let self else {
return return
} }
@ -663,23 +713,27 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
} }
var notices: [ButtonGroupView.Notice] = [] var notices: [ButtonGroupView.Notice] = []
if params.state.isLocalAudioMuted { if !isTerminated {
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "Your microphone is turned off")) 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.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.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 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 { if case .terminated = params.state.lifecycleState {
displayClose = true 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) 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? var expandedEmojiKeyRect: CGRect?
@ -1105,9 +1159,21 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
let titleString: String let titleString: String
switch params.state.lifecycleState { switch params.state.lifecycleState {
case .terminated: case let .terminated(terminatedState):
self.titleView.contentMode = .center 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.setScale(layer: self.blobLayer, scale: 0.3)
genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: 0.0) genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: 0.0)
self.canAnimateAudioLevel = false self.canAnimateAudioLevel = false
@ -1133,12 +1199,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
let statusState: StatusView.State let statusState: StatusView.State
switch params.state.lifecycleState { switch params.state.lifecycleState {
case .connecting: case .requesting:
statusState = .waiting(.requesting) statusState = .waiting(.requesting)
case .connecting:
statusState = .waiting(.connecting)
case .reconnecting:
statusState = .waiting(.reconnecting)
case .ringing: case .ringing:
statusState = .waiting(.ringing) statusState = .waiting(.ringing)
case .exchangingKeys:
statusState = .waiting(.generatingKeys)
case let .active(activeState): case let .active(activeState):
statusState = .active(StatusView.ActiveState(startTimestamp: activeState.startTime, signalStrength: activeState.signalInfo.quality)) 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, stableId: callUpdate.callId,
handle: "\(callUpdate.peer.id.id._internalGetInt64Value())", handle: "\(callUpdate.peer.id.id._internalGetInt64Value())",
phoneNumber: phoneNumber.flatMap(formatPhoneNumber), phoneNumber: phoneNumber.flatMap(formatPhoneNumber),
isVideo: false, isVideo: callUpdate.isVideo,
displayTitle: callUpdate.peer.debugDisplayTitle, displayTitle: callUpdate.peer.debugDisplayTitle,
completion: { error in completion: { error in
if let error = error { if let error = error {