From 820b038bbc779277b123e3ef2ecfab37037130c8 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 18 Dec 2023 02:06:39 +0400 Subject: [PATCH] [WIP] Call UI --- .../Sources/ManagedAudioSession.swift | 8 +- .../Sources/CallController.swift | 15 +- .../Sources/CallControllerNodeV2.swift | 70 +++++++-- .../Sources/CallKitIntegration.swift | 57 +++----- .../Sources/State/AccountStateManager.swift | 10 +- .../Sources/State/CallSessionManager.swift | 16 ++- .../Sources/Components/ButtonGroupView.swift | 6 +- .../Components/ContentOverlayButton.swift | 18 +-- .../Sources/Components/StatusView.swift | 15 +- .../Sources/PrivateCallScreen.swift | 136 +++++++++++++----- .../TelegramUI/Sources/AppDelegate.swift | 2 +- 11 files changed, 240 insertions(+), 113 deletions(-) diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index 45d2b6e15f..7b1431d82f 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -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 diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 15e0aa720a..fc2ea47e86 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -50,6 +50,9 @@ public final class CallController: ViewController { return self._ready } + private let isDataReady = Promise(false) + private let isContentsReady = Promise(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)) } } }) diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index d2b07adac5..7552742ac5 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -30,6 +30,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private let callScreen: PrivateCallScreen private var callScreenState: PrivateCallScreen.State? + let isReady = Promise() + 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) { diff --git a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift index 45a2f734ea..014180aa62 100644 --- a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift +++ b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift @@ -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) } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 1601b20c24..3f7469dabe 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index 51bb27ee04..9eb0b60a9e 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index 0e86f8f821..5da177d130 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift index e02f4c9d00..ac1ea32db8 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift index 2c79839627..f2949a4f91 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift @@ -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() diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 38dac04c1e..1d32ed6319 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -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)) diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 03b5f310a5..23cfac6d4f 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2030,7 +2030,7 @@ private func extractAccountManagerState(records: AccountRecordsView